Compare commits

..

258 Commits

Author SHA1 Message Date
Torsten Schulz (local)
52c7f1c7ba Refactor sendMessageToConnection method to enhance user data validation and message handling
- Introduce a local copy of the message to ensure its validity during processing.
- Validate user data retrieved from the WebSocket interface to ensure consistency before queuing messages.
- Streamline logging by removing redundant checks and focusing on critical error handling, improving overall clarity and stability.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
7a2749c405 Remove redundant exception handling in sendMessageToConnection method to streamline error logging and improve code clarity. 2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
d71df901ed Refactor message sending logic in WebSocket server to improve direct transmission and error handling
- Attempt to send messages directly during the RECEIVE callback to avoid mutex issues, enhancing performance.
- Implement size checks for messages to prevent overflow, with logging for oversized messages.
- Introduce additional error handling and logging for socket write operations, ensuring robust message delivery and queue management.
- Maintain thread safety by validating user data and mutex locking before queuing messages when direct sending fails.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
1af4b6c2e4 Enhance logging and error handling in sendMessageToConnection method
- Introduce detailed logging for the message sending process, including checks for user data validity and message queue status.
- Implement additional null checks for user data before and after locking the mutex to ensure thread safety.
- Ensure proper message copying to maintain validity during queuing, improving overall stability and error visibility.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
2595cb8565 Enhance error handling and logging in WebSocket server message sending
- Introduce detailed logging for message creation and sending processes, including message size and success confirmation.
- Implement comprehensive null checks for instance, WebSocket interface, and user data before invoking sendMessageToConnection, improving stability.
- Add exception handling to capture and log errors during message sending, enhancing visibility into potential issues.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
45d549aa4e Refactor message sending in WebSocket server to utilize sendMessageToConnection
- Replace manual message queuing and error handling with the sendMessageToConnection method, which consolidates necessary checks and improves code clarity.
- Remove redundant null checks and logging related to message queue access, streamlining the callback logic.
- Enhance overall stability by leveraging existing functionality for message delivery during WebSocket events.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
7f65f5e40e Enhance message queuing with improved error handling and logging in WebSocket server
- Implement detailed logging for message queuing attempts, including message size and copy operations.
- Add comprehensive null checks for user data and message queue validity before pushing messages to the queue.
- Introduce exception handling to manage potential errors during message queuing, improving stability and error visibility.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
5ce1cc4e6a Refine null checks and logging in WebSocket server message handling
- Introduce a local copy of user data before locking the mutex to ensure validity during message queuing.
- Enhance null checks and logging to provide clearer insights when user data or message queue access fails.
- Implement exception handling for message queue access to improve stability and error visibility.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
3a6d60e9a8 Improve null checks and logging in WebSocket server message handling
- Add additional null checks for user data before and after locking the mutex to prevent potential crashes.
- Enhance logging to provide clearer insights when user data is invalid during message queuing.
- Ensure proper message copying to a local variable before accessing the message queue, improving thread safety and stability.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
d5a09f359d Enhance logging and error handling in getConnections callback
- Add detailed logging to track the flow and validity of user data during the getConnections event.
- Implement exception handling to manage potential access issues with user data, improving stability and error visibility.
- Ensure clear output for both successful and failed user data access attempts, aiding in debugging and monitoring.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
127e95ca1c Improve null checks and logging in WebSocket server callbacks
- Add checks for user data to prevent null pointer exceptions during message handling.
- Enhance logging to provide clearer insights when user data is invalid or when exceptions occur.
- Ensure proper mutex locking when accessing the message queue to maintain thread safety.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
bb81126cd8 Enhance error response handling in WebSocket server
- Add detailed logging for error responses during WebSocket callbacks, improving visibility into the error handling process.
- Ensure that error responses are queued correctly without immediate sending, enhancing stability during callback execution.
- Utilize lws_cancel_service to notify the service of pending messages, ensuring proper message delivery after error handling.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
2d3d120f81 Refactor WebSocket server message queuing and error handling
- Implement message queuing for error responses during WebSocket callbacks to prevent immediate sending, enhancing stability.
- Utilize lws_cancel_service to trigger the writable callback safely, ensuring messages are sent correctly after the callback execution.
- Improve error handling and logging for message sending operations, providing clearer insights into potential issues.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
0c36c4a4e5 Refactor WebSocket server message handling to include user data
- Update sendMessageToConnection to accept user data, enhancing message delivery accuracy.
- Improve error handling in WebSocket callbacks by adding user data checks to prevent null pointer exceptions.
- Enhance logging for error responses to provide clearer insights into message handling issues.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
88f6686809 Enhance WebSocket server message handling and error responses
- Improve asynchronous message sending to prevent connection issues during callbacks.
- Add error response handling for failed connection retrieval, ensuring clients receive feedback on errors.
- Implement message size checks to prevent oversized messages from being sent, enhancing stability and reliability.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
9c7b682a36 Improve error handling and null checks in WebSocket server callbacks
- Add null checks for user data in various WebSocket callback functions to prevent crashes and improve stability.
- Enhance error logging to provide clearer insights into issues related to user data and connection management.
- Refactor the handling of active connections to ensure robust error handling during data processing and message sending.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
dafdbf0a84 Refactor WebSocket server to use nlohmann::json for active connections
- Update the return type of getActiveConnections() in both websocket_server.cpp and websocket_server.h to nlohmann::json for consistency and clarity.
- Ensure proper usage of the nlohmann::json library in the WebSocket server implementation.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
5ac8e9b484 Enhance WebSocket server connection management and error handling
- Introduce connection time tracking for WebSocket users to monitor connection duration.
- Implement user ID management to allow dynamic updates and removal of connections based on user ID changes.
- Add functionality to retrieve active connections, including unauthenticated ones, for administrative purposes.
- Improve error handling during connection closure and ensure proper cleanup of connection entries.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
753c5929e1 Refactor configuration file installation and template handling
- Update CMakeLists.txt to install the template configuration file as an example, ensuring it is available for reference.
- Modify install-config.cmake to prioritize the installed template file, with fallbacks to source directory templates if the installed one is missing, enhancing the robustness of the configuration setup.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
e3f46d775a Enhance WebSocket server ping/pong handling and timeout settings
- Introduce handling for LWS_CALLBACK_RECEIVE_PONG to manage Pong frames received from clients.
- Update the WebSocketUserData structure to increase MAX_PING_TIMEOUTS from 3 to 5, allowing more attempts before disconnection.
- Extend PONG_TIMEOUT_SECONDS from 10 to 60 to accommodate longer response times from browsers.
- Modify ping handling to send a WebSocket Ping frame instead of a text message for better protocol compliance.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
0eb3a78332 Enhance configuration file installation process
- Implement a CMake script for intelligent merging of configuration files, ensuring only missing keys are added without overwriting existing ones.
- Install a template configuration file as an example, preventing overwriting of the original during installation.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
3ac9f25284 Enhance WebSocket server socket options and connection handling
- Add support for setting SO_REUSEADDR in the WebSocket server to allow port reuse, improving server flexibility.
- Implement callbacks for socket adoption to ensure SO_REUSEADDR is set when applicable.
- Refine server options to streamline connection management and enhance overall performance.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
b3c9c8f37c Improve WebSocket server startup and error reporting
- Introduce a brief wait time to ensure the port is released before starting the server.
- Update server options to allow port reuse, enhancing server flexibility.
- Enhance error handling during context creation to provide more informative error messages regarding port usage and permissions.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
32bc126def Enhance WebSocket server options and error handling
- Update server options to support multiple simultaneous connections and improve security practices.
- Allow multiple connections per IP with configurable keep-alive settings.
- Improve error handling during WebSocket service operations, ensuring critical errors lead to server shutdown.
- Refine connection closure logic to handle user IDs more robustly and log connection states accurately.
- Enable WebSocket upgrade requests while rejecting other HTTP requests for better protocol management.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
00a5f47cae Refactor WebSocket server connection management and message handling
- Update WebSocketUserData to use a message queue for handling outgoing messages, improving concurrency and message delivery.
- Modify pingClients method to handle multiple connections per user and implement timeout logic for ping responses.
- Enhance addConnection and removeConnection methods to manage multiple connections for each user, including detailed logging of connection states.
- Update handleBrokerMessage to send messages to all active connections for a user, ensuring proper queue management and callback invocation.
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
6a1260687b Implement comprehensive character deletion process in UserCharacterWorker
- Add queries and logic to delete associated data when a character dies, including directors, relationships, child relations, knowledge, debtors prism, political offices, and election candidates.
- Enhance error handling to log issues during the deletion process.
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
7591787583 Update configuration file path for daemon in main.cpp 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
bd961a03d4 Aktualisiere WebSocket-Server und Daemon-Konfiguration
- Ändere die Pfade für SSL-Zertifikate in der Konfigurationsdatei.
- Verbessere die Fehlerbehandlung beim Entfernen alter vorbereiteter Anweisungen in HouseWorker.
- Füge Debug-Ausgaben zur Nachverfolgung von Verbindungen und Nachrichten im WebSocket-Server hinzu.
- Implementiere Timeout-Logik für das Stoppen von Worker- und Watchdog-Threads.
- Optimiere die Signalverarbeitung und Shutdown-Logik in main.cpp für bessere Responsivität.
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8fe816dddc WebSocket-Verbindungsverwaltung implementiert
- User-ID wird bei setUserId Event gespeichert
- Verbindungen werden in connections Map verwaltet
- Nachrichten werden über pendingMessage gesendet
- Statische Instanz-Referenz für Callback-Zugriff
- Explizite JSON-Konvertierung für Kompatibilität
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
e7a8dc86eb Füge Unterstützung für die Verwaltung von WebSocket-Verbindungen hinzu. Implementiere Methoden zum Hinzufügen und Entfernen von Verbindungen basierend auf Benutzer-IDs. Aktualisiere die WebSocket-Callback-Logik, um empfangene Nachrichten zu verarbeiten und Benutzer-IDs zu setzen. Verbessere die Ausgabe von Debug-Informationen zur Nachverfolgung von Verbindungen und Nachrichten. 2026-01-14 14:38:42 +01:00
Torsten (PC)
c9dc891481 updated rights 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
89c3873db7 Füge Überprüfung auf sudo-Rechte im SSL-Setup-Skript hinzu und aktualisiere die Pfade für Apache2-Zertifikate. Priorisiere Let's Encrypt-Zertifikate und füge Warnungen für Snakeoil-Zertifikate hinzu, um Benutzer über deren Einschränkungen zu informieren. Aktualisiere die Dokumentation entsprechend. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
60352d7932 Erweitere das SSL/TLS Setup-Skript um Unterstützung für Apache2-Zertifikate. Füge eine neue Funktion zum Einrichten und Verlinken von Apache2-Zertifikaten hinzu, einschließlich der Überprüfung auf vorhandene Zertifikate und der automatischen Erneuerung für Let's Encrypt. Aktualisiere die Benutzerführung zur Auswahl von Zertifikatstypen und dokumentiere die neuen Optionen in der SSL-Setup-Dokumentation. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
664f2af346 Erweitere das SSL/TLS Setup-Skript für den YourPart Daemon um Unterstützung für Let's Encrypt. Füge Funktionen zur Erstellung und Erneuerung von Let's Encrypt Zertifikaten hinzu, einschließlich automatischer Erneuerung über Cron Jobs. Ermögliche die Auswahl zwischen Self-Signed und Let's Encrypt Zertifikaten und verbessere die Benutzerführung bei der Zertifikatsauswahl. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8212e906a3 Füge Unterstützung für SSL/TLS in den WebSocket-Server hinzu. Aktualisiere die Konfigurationsdatei, um SSL-Optionen zu ermöglichen, und passe die WebSocketServer-Klasse an, um Zertifikat- und Schlüsselpfade zu akzeptieren. Verbessere die Serverstartlogik, um SSL korrekt zu initialisieren und entsprechende Meldungen auszugeben. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
92e17a9f43 Verbessere die Verwaltung der Konfigurationsdatei im Skript deploy-server.sh. Füge eine Überprüfung hinzu, ob die Konfigurationsdatei existiert, und kopiere sie nur, wenn sie nicht vorhanden ist. Ergänze die Logik zum Hinzufügen fehlender Schlüssel in die bestehende Konfigurationsdatei. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
d3727ad2f7 Ändere den Typ des Services in der Datei yourpart-daemon.service von 'simple' auf 'notify' und füge die Option NotifyAccess hinzu. Verbessere die Signalverarbeitung in main.cpp, um ein sauberes Herunterfahren der Anwendung zu ermöglichen und die Hauptschleife anzupassen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
391e5d9992 Ändere den Typ des Services in der Datei yourpart-daemon.service von 'notify' auf 'simple' für eine verbesserte Service-Verwaltung. Füge im Skript deploy-server.sh eine Verzögerung von 3 Sekunden nach dem Start des Services hinzu, um sicherzustellen, dass der Dienst ordnungsgemäß initialisiert wird. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
a4bd585730 Füge Überprüfung und Stopp des Services vor dem Kopieren der Dateien im Skript deploy-server.sh hinzu. Aktualisiere die Nummerierung der Schritte für eine bessere Übersichtlichkeit und entferne die Überprüfung, ob der Service bereits läuft, bevor er neu gestartet wird. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
c694769f4c Füge Überprüfung der Root-Rechte hinzu und aktualisiere Berechtigungen im Skript deploy-server.sh. Alle relevanten Operationen, die erhöhte Rechte benötigen, werden nun mit sudo ausgeführt, um die Sicherheit und Funktionalität zu verbessern. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8b9ff9793c Verbessere die Statusverarbeitung in der Methode spyIn, indem die Zuweisung des Status aus dem JSON-Objekt optimiert wird. Verwende nun die get-Methode für eine klarere und sicherere Zuweisung. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8ba4566d23 Füge Unterstützung für systemd hinzu, indem die systemd-Entwicklungslibraries in CMakeLists.txt und Installationsskripten für OpenSUSE und Ubuntu 22 integriert werden. Aktualisiere die yourpart-daemon.service-Datei für eine verbesserte Service-Verwaltung und implementiere die Benachrichtigung an systemd, wenn der Dienst bereit ist. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
91420b9973 Erweitere die Unterstützung für vorbereitete Abfragen in der Datenbank, indem die Methode exec_params für bis zu 10 Parameter implementiert wird. Füge eine Fehlerbehandlung für zu viele Parameter hinzu. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8d3e0423e7 Füge Unterstützung für verschiedene Versionen von libpqxx hinzu, um die Kompatibilität mit libpqxx 6.x und 7.x zu gewährleisten. Implementiere unterschiedliche Methoden zur Ausführung vorbereiteter Abfragen basierend auf der Anzahl der Parameter. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
4bafc3a61c Verbessere die CMake-Konfiguration zur Unterstützung von C++23, indem die Compiler-Auswahl dynamisch auf GCC 15 oder 13 basiert. Optimiere die Compiler-Flags für Leistung. In der Datenbankabfrage und im DirectorWorker werden konstante Referenzen und string_view verwendet, um die Leistung zu steigern. Reserviere Speicher für Vektoren in main.cpp zur Effizienzsteigerung. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
1f43df6d41 Ändere die Berechtigungen des Skripts deploy-server.sh von 644 auf 755, um die Ausführbarkeit zu ermöglichen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
c2a54e29f8 Aktualisiere die Compiler-Version in CMakeLists.txt und install-dependencies-ubuntu22.sh von GCC 15 auf GCC 13 für bessere Unterstützung von C++23. Passe die Installationsmeldungen und Standard-Compiler-Einstellungen entsprechend an. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
b1f9073f4d Ändere die Berechtigungen des Skripts install-dependencies-ubuntu22.sh von 644 auf 755, um die Ausführbarkeit zu ermöglichen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
1b38e2412c Aktualisiere das Build-Skript in deploy.sh, um den C++ Standard auf Version 23 zu setzen. Ändere die Installation des C++ Compilers in install-dependencies-ubuntu22.sh, um GCC 15 zu installieren und als Standard-Compiler festzulegen. Entferne die vorherige Installation von GCC 11. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
4b9311713a Aktualisiere das Build-Skript, um C++ Standard auf Version 20 zu setzen. Ändere die Installation des C++ Compilers in install-dependencies.sh, um GCC 11 als Standard für Ubuntu 22 zu verwenden und entferne die Installation von GCC 15. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
77520ee46a Ändere die Berechtigungen der Skripte deploy.sh und install-dependencies.sh von 644 auf 755, um die Ausführbarkeit zu ermöglichen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
23c07a3570 Füge UndergroundWorker hinzu und implementiere Logik für unterirdische Aufgaben. Aktualisiere CMakeLists.txt, um neue Quell- und Header-Dateien einzuschließen. Verbessere die Fehlerbehandlung in der Datenbank und sende Benachrichtigungen nach bestimmten Ereignissen. Integriere Hilfsfunktionen zur sicheren Verarbeitung von Daten. 2026-01-14 14:38:35 +01:00
Torsten Schulz
1451225978 stabilized app 2026-01-14 14:37:21 +01:00
Torsten Schulz
51fd9fcd13 Ändere die Überprüfung auf die Erstellung von Charakteren von "vorheriger Tag" zu "heutiger Tag" 2026-01-14 14:37:21 +01:00
Torsten (PC)
1fe77c0905 fix 2026-01-14 14:36:57 +01:00
Torsten Schulz (local)
cd739fb52e Refactor server.js for improved WebSocket and API configurations
- Updated API server to listen on a configurable port (2020) for better flexibility.
- Removed WebSocket setup from HTTP server, ensuring Socket.io is only available over HTTPS.
- Enhanced logging to clarify Socket.io availability based on TLS configuration.
2026-01-14 14:30:50 +01:00
Torsten Schulz (local)
9e845843d8 Update WebSocket and API configurations in yourpart-websocket-fixed.conf and daemonServer.js
- Adjusted WebSocket proxy settings in yourpart-websocket-fixed.conf to route traffic through port 4551 for both secure and non-secure connections.
- Enhanced daemonServer.js to listen on all interfaces (0.0.0.0) for both TLS and non-TLS WebSocket connections, improving accessibility.
2026-01-14 13:10:33 +01:00
Torsten Schulz (local)
0cc280ed55 Refactor WebSocket and API configurations in yourpart-https.conf and yourpart-websocket-fixed.conf
- Removed outdated WebSocket handling from yourpart-https.conf for improved clarity.
- Updated yourpart-websocket-fixed.conf to enable SSL and adjust WebSocket proxy settings.
- Streamlined fallback logic in frontend store to ensure direct connection to the daemon on port 4551.
- Enhanced logging for better debugging and monitoring of daemon connections.
2026-01-14 13:02:38 +01:00
Torsten Schulz (local)
b3707d21b2 Refactor yourpart-https.conf for enhanced WebSocket and API request handling
- Updated www redirect to exclude API and WebSocket paths for improved functionality.
- Organized WebSocket and API proxy settings within Location blocks for better clarity and maintainability.
- Consolidated WebSocket upgrade rules for Socket.io and daemon connections, ensuring consistent handling.
2026-01-14 12:07:04 +01:00
Torsten Schulz (local)
fbebd6c1c1 Refactor yourpart-https.conf for improved WebSocket handling and domain redirection
- Updated www redirect to exclude WebSocket paths for better functionality.
- Consolidated WebSocket upgrade rules for Socket.io and daemon connections.
- Enhanced organization of proxy settings for API and WebSocket requests, improving maintainability.
2026-01-14 12:02:49 +01:00
Torsten Schulz (local)
d7c2bda461 Enhance yourpart-https.conf with improved WebSocket and API configurations
- Added www redirect to ensure consistent domain usage.
- Consolidated WebSocket upgrade conditions for clarity.
- Streamlined API request forwarding and fallback proxy settings for better organization and maintainability.
2026-01-14 11:57:35 +01:00
Torsten Schulz (local)
2bf949513b Refactor yourpart-https.conf for improved WebSocket and API request handling
- Consolidated WebSocket upgrade conditions for clarity and consistency.
- Streamlined API request forwarding configuration.
- Removed redundant proxy settings to enhance organization and maintainability of the configuration file.
2026-01-14 10:28:23 +01:00
Torsten Schulz (local)
84619fb656 Update proxy settings in yourpart-https.conf for improved WebSocket and API handling
- Added ProxyPreserveHost and ProxyRequests directives for better request handling.
- Configured WebSocket upgrade headers for Socket.io and daemon connections.
- Established HTTP proxies for API and WebSocket requests to ensure fallback mechanisms are in place.
- Improved overall clarity and organization of proxy settings in the configuration file.
2026-01-12 16:55:09 +01:00
Torsten Schulz (local)
b600f16ecd Enhance MessagesDialog component and localization for overproduction notifications
- Updated MessagesDialog.vue to extract additional parameters (value, branch_id, region_id) for better handling of overproduction scenarios.
- Modified localization files (de/falukant.json and en/falukant.json) to reflect changes in the overproduction notification format, including branch information.
- Improved data formatting for clarity in notifications related to production levels.
2026-01-12 16:48:10 +01:00
Torsten Schulz (local)
9273066f61 Refactor trait handling in FalukantService and FamilyView for improved data consistency
- Updated trait loading in FalukantService to include trait types, enhancing the mapping of traits to characters.
- Adjusted FamilyView to utilize the new trait structure, ensuring accurate retrieval of traits associated with characters.
- Improved null checks for relationships to prevent potential errors during data access.
2026-01-12 13:48:37 +01:00
Torsten Schulz (local)
7d59dbcf84 Update mood and character traits handling in FalukantService to ensure default values are set when no data is available. This change improves robustness in data retrieval by preventing potential undefined values. 2026-01-12 13:44:29 +01:00
Torsten Schulz (local)
015d1ae95b Refactor getGiftCost method in FalukantService for improved performance
- Changed getGiftCost from an asynchronous to a synchronous method, eliminating unnecessary await for better efficiency.
- Updated comments for clarity regarding the synchronous nature of gift cost calculation.
- Adjusted related code to reflect the synchronous execution, enhancing overall performance in gift cost retrieval.
2026-01-12 12:02:46 +01:00
Torsten Schulz (local)
e2cd6e0e5e Refactor relationship retrieval in FalukantService for improved performance
- Optimized the process of finding relationships by using two separate queries for better index utilization, reducing data overhead.
- Enhanced loading of related characters and traits by implementing parallel data fetching, improving efficiency in data retrieval.
- Updated timing metrics to reflect changes in the relationship loading process, ensuring accurate performance tracking.
2026-01-12 11:57:17 +01:00
Torsten Schulz (local)
ec113058d0 Enhance getGifts method in FalukantService with detailed performance metrics and optimized data retrieval
- Implemented timing metrics to track performance across various steps of the getGifts method, improving traceability.
- Refactored data loading to optimize user, character, and relationship queries, ensuring only necessary fields are fetched.
- Enhanced error handling and logging for better debugging and performance insights during execution.
2026-01-12 11:49:49 +01:00
Torsten Schulz (local)
d2ac2bfdd8 Optimize gift retrieval in FalukantService by loading only necessary fields and implementing parallel data fetching. This change enhances performance and reduces data overhead during the gift and title of nobility retrieval process. 2026-01-12 11:46:16 +01:00
Torsten Schulz (local)
d75fe18e6a Optimize user and character loading in FalukantService by querying only necessary fields. This change enhances performance and reduces data overhead during retrieval. 2026-01-12 11:35:38 +01:00
Torsten Schulz (local)
479f222b54 Refactor character retrieval in FalukantService for improved clarity
- Changed the character retrieval method to directly access the user's character property instead of querying the database, enhancing code readability and performance.
2026-01-12 11:31:49 +01:00
Torsten Schulz (local)
013c536b47 Refactor firstNameMap creation in FalukantService for improved efficiency
- Simplified the construction of firstNameMap by removing unnecessary spread operator, enhancing performance and readability.
2026-01-12 11:28:32 +01:00
Torsten Schulz (local)
3b983a0db5 Update attribute mapping in FalukantService for mood data retrieval
- Changed mood attribute from 'labelTr' to 'tr' in the mood query to align with updated data structure.
- Adjusted mood mapping to reflect the new attribute, ensuring consistency in data handling.
2026-01-12 11:24:56 +01:00
Torsten Schulz (local)
5f9559ac8d Update FalukantService to utilize FalukantCharacterTrait for character relationships
- Added import for FalukantCharacterTrait to enhance data retrieval for character relationships.
- Refactored the relationship character query to use FalukantCharacterTrait instead of CharacterTrait, improving data accuracy and consistency.
2026-01-12 11:16:23 +01:00
Torsten Schulz (local)
f487e6d765 Enhance getFamily method in FalukantService for performance and data retrieval
- Implemented detailed timing metrics to track performance across various steps of the getFamily method.
- Refactored data retrieval to load relationships and children in parallel, improving efficiency.
- Enhanced mapping of relationship and child data, including additional attributes for characters.
- Introduced batch loading for related data to minimize database queries and optimize performance.
- Improved error handling and logging for better traceability during execution.
2026-01-12 11:09:21 +01:00
Torsten Schulz (local)
5e26422e9c Add batch price retrieval for products in region
- Implemented a new endpoint to fetch prices for multiple products in a specified region in a single request, improving efficiency.
- Added validation for input parameters to ensure proper data handling.
- Updated the FalukantService to calculate prices based on knowledge factors and worth percentages for each product.
- Modified the frontend to utilize the new batch endpoint, optimizing the loading of product prices.
2026-01-12 08:58:28 +01:00
Torsten Schulz (local)
64baebfaaa Optimize proposal generation in FalukantService using CTEs
- Replaced multiple SQL queries with a single query utilizing Common Table Expressions (CTEs) for improved performance.
- Streamlined the exclusion of existing proposals and active directors directly within the SQL query.
- Enhanced logging for SQL queries and results, providing better traceability during proposal generation.
- Simplified the process of calculating average knowledge for character proposals, ensuring more efficient data handling.
2026-01-12 08:46:54 +01:00
Torsten Schulz (local)
521dec24b2 Enhance proposal generation logic in FalukantService
- Implemented exclusion of existing proposals and active directors to avoid duplicate character selections.
- Added detailed logging for SQL queries and fallback mechanisms when insufficient older characters are found.
- Improved character selection process by combining excluded character IDs and ensuring a diverse set of proposals.
- Streamlined the batch creation of director proposals with average knowledge calculations for better income predictions.
2026-01-12 08:39:41 +01:00
Torsten Schulz (local)
36f0bd8eb9 Refactor MessagesDialog component to improve parameter interpolation and description formatting
- Enhanced the description formatting logic to ensure proper interpolation of parameters in both title and description fields.
- Updated the handling of monetary values to ensure they are correctly converted to numbers before formatting.
- Improved comments for clarity on the interpolation process and effects handling.
2026-01-09 14:44:20 +01:00
Torsten Schulz (local)
d0a2b122b2 Implement enhanced partner search and NPC creation logic in FalukantService
- Added detailed logging for partner search criteria and results, improving traceability.
- Refactored partner search logic to include a fallback mechanism for searching across all regions if no partners are found in the specified region.
- Introduced a new method for creating NPCs when no suitable partners are available, ensuring a continuous flow in the partner matching process.
- Improved the handling of character attributes such as age and title of nobility during partner searches and NPC creation.
2026-01-09 14:37:55 +01:00
Torsten Schulz (local)
c80cc8ec86 Enhance logging and error handling in FalukantService and FamilyView
- Added detailed logging for partner search and creation processes in FalukantService to improve traceability and debugging.
- Refactored the partner search logic to use a dynamic where clause for better readability and maintainability.
- Implemented error handling in FamilyView's loadGifts method to ensure an empty array is returned on API errors, enhancing user experience.
2026-01-09 14:32:27 +01:00
Torsten Schulz (local)
3722bcf8c8 Enhance parameter extraction in MessagesDialog component for money and effect changes
- Added extraction of money_change_absolute and money_change_percent from value to parameters.
- Updated effects handling to include money_change, storage_damage, production_quality_change, transport_speed_change, and storage_capacity_change types.
- Improved logic to prevent overwriting parameters already extracted from value, ensuring accurate data representation.
2026-01-09 09:31:33 +01:00
Torsten Schulz (local)
0372d213c0 Refine city filtering in NPC creation process within AdminService
- Added explicit filtering for city-type regions to ensure only valid cities are processed during NPC creation.
- Enhanced logging to provide feedback on the number of cities found and their names, improving traceability in the NPC creation workflow.
- Updated comments for clarity on the importance of using city regions exclusively.
2026-01-07 17:15:17 +01:00
Torsten Schulz (local)
c322eb1e5a Add NPC creation status tracking and progress reporting in Admin module
- Implemented getNPCsCreationStatus method in AdminController to retrieve the status of NPC creation jobs.
- Enhanced AdminService to manage NPC creation jobs, including job ID generation, progress updates, and error handling.
- Updated frontend CreateNPCView to display progress of NPC creation, including estimated time remaining and job status.
- Added localization strings for progress reporting in both German and English.
- Improved overall user experience by providing real-time feedback during NPC creation processes.
2026-01-07 17:09:54 +01:00
Torsten Schulz (local)
b34dcac685 Refactor CreateNPCView layout for improved structure and styling
- Updated the template structure in CreateNPCView.vue to enhance layout with additional div wrappers for better styling and organization.
- Ensured the main content is encapsulated within a scrollable area, improving user experience during NPC creation.
2026-01-07 17:00:56 +01:00
Torsten Schulz (local)
4850f50c66 Update package-lock.json dependencies for improved stability and security
- Upgraded 'glob' to version 10.5.0.
- Updated 'body-parser' to version 1.20.4 and adjusted its dependencies.
- Enhanced 'express' to version 4.22.1 with updated dependencies.
- Bumped 'qs' to version 6.14.1 and modified its dependencies.
- Updated 'raw-body' to version 2.5.3 and adjusted its dependencies.
- Ensured compatibility with newer versions of dependencies across the project.
2026-01-07 16:59:42 +01:00
Torsten Schulz (local)
5996f819e8 Enhance NPC creation functionality and validation in Admin module
- Updated AdminController to validate the count parameter, ensuring it is between 1 and 500.
- Refactored NPC creation logic in AdminService to create NPCs for each city-title combination, improving efficiency.
- Enhanced frontend localization files to reflect changes in count descriptions and validation messages.
- Updated CreateNPCView to provide user guidance on count input and display detailed creation results.
2026-01-07 16:57:50 +01:00
Torsten Schulz (local)
4d967fe7a2 Update German and English navigation localization files to include user rights translations 2026-01-07 16:49:33 +01:00
Torsten Schulz (local)
bb91c2bbe5 Add NPC creation and titles retrieval functionality in Admin module
- Implemented createNPCs method in AdminController to handle NPC creation with specified parameters including region, age, title, and count.
- Added getTitlesOfNobility method in AdminController to retrieve available titles for users.
- Updated adminRouter to include new routes for creating NPCs and fetching titles.
- Enhanced navigationController and frontend localization files to support new NPC creation feature.
- Introduced corresponding UI components and routes for NPC management in the admin interface.
2026-01-07 16:45:39 +01:00
Torsten Schulz (local)
511df52c3c Enhance MessagesDialog component to support HTML content and improve parameter extraction
- Updated notification description rendering to allow HTML content using v-html directive.
- Refactored formatBody method to better handle JSON formatted notifications and extract parameters from nested structures.
- Introduced new method for extracting parameters from value objects, improving compatibility with various notification types.
- Enhanced description formatting to include details from effects, providing richer user feedback in notifications.
2026-01-07 12:09:25 +01:00
Torsten Schulz (local)
d42e1da14b Refactor character creation and heir fetching logic in FalukantService and OverviewView
- Updated character creation process to utilize transactions for improved data integrity during heir generation.
- Enhanced heir fetching logic in OverviewView to check for both mainBranchRegion.id and mainBranchRegionId, adding error handling for missing regions.
- Added warnings for empty heir responses from the API to improve debugging and user feedback.
2026-01-07 11:20:03 +01:00
Torsten Schulz (local)
75dbd78da1 Add regional event handling and character creation logic in FalukantService
- Introduced new regional events ('regional_storm', 'regional_epidemic', 'earthquake') with specific health impact mechanics to enhance gameplay dynamics.
- Updated health modification logic to ensure regional events cause moderate health loss rather than fatal outcomes.
- Implemented character creation logic to generate potential heirs when none are found, including random gender, names, and age attributes.
- Enhanced heir retrieval process to include newly created characters, ensuring a seamless user experience.
2026-01-07 11:13:54 +01:00
Torsten Schulz (local)
c90b7785c0 Add heir selection functionality in Falukant module
- Implemented getPotentialHeirs and selectHeir methods in FalukantService to allow users to retrieve and select potential heirs based on specific criteria.
- Updated FalukantController to wrap new methods with user authentication and added corresponding routes in FalukantRouter.
- Enhanced OverviewView component to display heir selection UI when no character is present, including loading states and error handling.
- Added translations for heir selection messages in both German and English locales to improve user experience.
2026-01-07 10:29:16 +01:00
Torsten Schulz (local)
c17af04cbf Refactor vocabulary search functionality in VocabService and update UI components
- Modified the searchVocabs method in VocabService to consolidate search parameters into a single query term for improved flexibility.
- Updated VocabSearchDialog to replace separate input fields for mother tongue and learning language with a unified search term input.
- Adjusted button logic to enable search only when a term is provided, enhancing user experience.
- Added new translations for the search term in both German and English locales to support the updated UI.
2026-01-05 16:58:18 +01:00
Torsten Schulz (local)
f5e3a9a4a2 Add search functionality for vocabulary in VocabController and VocabService
- Implemented a new searchVocabs method in VocabService to allow users to search for vocabulary based on learning and mother tongue terms.
- Updated VocabController to include the searchVocabs method wrapped with user authentication.
- Added a new route in vocabRouter for searching vocabulary by language ID.
- Enhanced VocabChapterView and VocabLanguageView components to include a button for opening the search dialog.
- Added translations for search-related terms in both German and English locales, improving user accessibility.
2026-01-05 16:53:38 +01:00
Torsten Schulz (local)
dab3391aa2 Refactor socket.io URL normalization logic for improved clarity and robustness
- Simplified the normalization process for socket.io URLs by removing unnecessary production checks.
- Enhanced comments for better understanding of URL handling, particularly regarding environment variables and port management.
- Maintained fallback mechanisms for URL parsing failures to ensure consistent behavior across environments.
2026-01-05 16:43:42 +01:00
Torsten Schulz (local)
0336c55560 Enhance socket.io URL handling for production environments
- Added logic to normalize the socket.io URL based on the current origin and environment.
- Implemented fallback mechanisms for unusual ports in production to ensure secure connections.
- Included error handling for URL parsing failures to default to the current origin, improving robustness.
2026-01-05 16:26:23 +01:00
Torsten Schulz (local)
8e618ab443 Implement TLS support in WebSocket server for secure connections
- Added environment variable configuration for enabling TLS in the WebSocket server.
- Implemented logic to read TLS key, certificate, and optional CA paths from environment variables.
- Enhanced server initialization to handle both secure (WSS) and non-secure (WS) connections based on the TLS setting.
- Included error handling for missing TLS configuration to prevent server startup failures.
2026-01-05 16:06:37 +01:00
Torsten Schulz (local)
352d672bdd Add keyboard handling in VocabPracticeDialog for improved user interaction
- Implemented keydown event listener to manage Enter key functionality for navigating questions and submitting answers.
- Enhanced user experience by allowing Enter to trigger actions based on the current dialog state, improving accessibility during practice sessions.
- Ensured proper cleanup of event listeners when closing the dialog to prevent memory leaks.
2025-12-30 20:22:04 +01:00
Torsten Schulz (local)
df64c0a4b5 Update VocabPracticeDialog and VocabChapterView components to manage practice dialog state
- Changed the close button visibility in VocabPracticeDialog to false for a cleaner UI.
- Enhanced VocabChapterView to manage the practice dialog state with a new `practiceOpen` flag.
- Updated the `openPractice` method to handle the dialog's open and close events, ensuring proper state management.
2025-12-30 18:44:35 +01:00
Torsten Schulz (local)
83597d9e02 Add Vocab Trainer feature with routing, database schema, and translations
- Introduced Vocab Trainer functionality, including new routes for managing languages and chapters.
- Implemented database schema for vocab-related tables to ensure data integrity.
- Updated navigation and UI components to include Vocab Trainer in the social network menu.
- Added translations for Vocab Trainer in both German and English locales, enhancing user accessibility.
2025-12-30 18:34:32 +01:00
Torsten Schulz (local)
a09220b881 Add translations for reputation action school funding in German and English locales 2025-12-23 14:37:33 +01:00
Torsten Schulz (local)
5623f3af09 Refactor error handling in FalukantService and enhance user feedback in HealthView
- Changed error throwing in FalukantService to use PreconditionError for better clarity.
- Added translations for "too close" error in both German and English locales.
- Improved user feedback in HealthView by displaying error messages in a dialog upon measure execution failure.
2025-12-23 12:20:37 +01:00
Torsten Schulz (local)
820b5e8570 Enhance health management in FalukantService to improve user feedback and error handling
- Updated health deduction logic to clarify the distinction between health and monetary costs.
- Added error handling for money update failures to ensure robust transaction processing.
- Modified return value to include updated health status, providing clearer information for UI updates.
2025-12-23 10:51:22 +01:00
Torsten Schulz (local)
dc72ed2feb Add taxFromSaleProduct translation and enhance MoneyHistoryView for product loading
- Added new translation keys for "taxFromSaleProduct" in both German and English locales.
- Updated MoneyHistoryView component to load product data asynchronously and map product IDs to their labels for improved activity translation handling.
- Enhanced activity translation logic to support both legacy and new formats for tax-related activities.
2025-12-22 15:17:51 +01:00
Torsten Schulz (local)
ea468c9878 Improve conditionLabel method in BranchView component to handle edge cases and ensure accurate state representation. Added checks for null and undefined values, and clarified the return value for zero or negative conditions. 2025-12-22 14:54:51 +01:00
Torsten Schulz (local)
d1b683344e Update condition handling in FalukantService and syncDatabase utility for legacy data
- Enhanced condition processing in FalukantService to clamp values between 0 and 100, ensuring UI displays valid data.
- Implemented database cleanup in syncDatabase utility to set NULL conditions to 100 and clamp out-of-range values, improving data integrity.
2025-12-22 13:20:16 +01:00
Torsten Schulz (local)
a82ec7de3f Add cooldown feature for reputation actions in FalukantService and update UI components
- Introduced a cooldown mechanism for reputation actions, limiting execution to once per configured interval.
- Updated FalukantService to handle cooldown logic and return remaining cooldown time.
- Enhanced ReputationView component to display cooldown status and prevent action execution during cooldown.
- Added translations for cooldown messages in both German and English locales.
2025-12-21 22:18:29 +01:00
Torsten Schulz (local)
560a9efc69 Refactor ReputationView component to streamline action display and remove deprecated tab
- Added a new section for displaying reputation actions, including daily limits and action details.
- Removed the 'actions' tab from the navigation and adjusted the logic to reflect this change.
- Enhanced the user interface for executing reputation actions with improved button states and translations.
2025-12-21 21:37:22 +01:00
Torsten Schulz (local)
4f8b1e33fa Update message dialog parameters in ReputationView component for improved clarity
- Swapped the order of parameters in the message dialog open method to prioritize message content over the title, enhancing user experience during reputation action notifications.
2025-12-21 21:14:06 +01:00
Torsten Schulz (local)
38dd51f757 Add reputation actions feature to Falukant module
- Introduced new endpoints for retrieving and executing reputation actions in FalukantController and falukantRouter.
- Implemented service methods in FalukantService to handle reputation actions, including daily limits and action execution logic.
- Updated the frontend ReputationView component to display available actions and their details, including cost and potential reputation gain.
- Added translations for reputation actions in both German and English locales.
- Enhanced initialization logic to set up reputation action types in the database.
2025-12-21 21:09:31 +01:00
Torsten Schulz (local)
38f23cc6ae Enhance getFalukantUserOrFail and createParty methods in FalukantService to support transaction options
- Updated getFalukantUserOrFail to accept an options parameter for transaction handling.
- Refactored createParty to utilize transaction support, ensuring atomic operations for party creation and related financial transactions.
- Improved error handling for party creation, including checks for existing parties within a 24-hour window and validation of selected options.
2025-12-20 23:30:10 +01:00
Torsten Schulz (local)
6cf8fa8a9c Add reputation attribute to FalukantCharacter model and update related services and views
- Introduced a new 'reputation' attribute in the FalukantCharacter model with a default value and validation.
- Updated FalukantService to include 'reputation' in character attributes for API responses.
- Enhanced ReputationView component to display current reputation and load it from the API.
- Added translations for reputation in both German and English locales.
2025-12-20 23:00:31 +01:00
Torsten Schulz (local)
f9ea4715d7 Refactor BranchView component to replace JavaScript alerts with a message dialog for success and error notifications. This improves user experience by providing a more integrated feedback mechanism within the UI. 2025-12-20 22:14:39 +01:00
Torsten Schulz (local)
b34b374f76 Refactor sellAllProducts method in FalukantService to ensure atomic transactions for selling products, updating inventory, and handling financial transactions. Implement batch processing for sell items and enhance error handling for inventory deletions. Update updateFalukantUserMoney function to support transactions, improving consistency and reliability in financial operations. 2025-12-20 22:04:29 +01:00
Torsten Schulz (local)
83d1168f26 Refactor speedLabel method in SaleSection component to move it from computed properties to methods for better compatibility with Vue3. This change ensures the function is callable and maintains its intended functionality. 2025-12-20 21:32:53 +01:00
Torsten Schulz (local)
91009f52cd Refactor SaleSection component to utilize direct property assignment for betterPrices, enhancing reactivity in Vue3. Update inventory mapping to ensure betterPrices is always an array. 2025-12-20 21:28:01 +01:00
Torsten Schulz (local)
c6dfca7052 Enhance SaleSection component to improve UI responsiveness during sales. Reset selling state immediately after sell actions and update inventory handling to ensure user feedback is timely and accurate. 2025-12-20 21:09:10 +01:00
Torsten Schulz (local)
aaeaeeed24 Add request and SQL performance logging features to backend
- Implement request timing middleware in app.js to log slow requests and all requests based on environment variables.
- Enhance sequelize.js with optional SQL query timing and logging capabilities, allowing for performance monitoring of database queries.
2025-12-20 16:35:30 +01:00
Torsten Schulz (local)
c5804f764c Optimize getInventory method in FalukantService by replacing nested includes with a single SQL query for improved performance. Add error handling for invalid branchId input. 2025-12-20 16:13:33 +01:00
Torsten Schulz (local)
fbe0d1bcd1 Add error handling for missing branches in sell batch processing in FalukantService. Ensure that missing branch IDs trigger an error to prevent accounting mismatches. 2025-12-20 16:01:18 +01:00
Torsten Schulz (local)
2fb440f033 Implement synchronous price calculation for batch operations in FalukantService, optimizing performance by reducing database queries. Update inventory handling to batch delete items and enhance revenue calculations. Fix translation formatting in German locale for sellAllSuccess message. 2025-12-20 15:37:16 +01:00
Torsten Schulz (local)
a8a136a9ce Enhance SaleSection component to manage selling state with improved user feedback. Disable buttons during selling, show status messages for sellAll actions, and update translations for new statuses. 2025-12-20 15:35:20 +01:00
Torsten Schulz (local)
fcbb903839 Backend error fixed 2025-12-20 15:06:43 +01:00
Torsten Schulz (local)
ac45a2ba26 Refactor SQL joins in calcRegionalSellPrice function of FalukantService to use updated region type table for improved clarity and consistency in tax calculations. 2025-12-20 15:03:03 +01:00
Torsten Schulz (local)
afe15dd4f5 Refactor tax calculation in calcRegionalSellPrice function of FalukantService to convert exemptTypes Set to PostgreSQL array string for improved query performance and clarity. 2025-12-20 14:54:32 +01:00
Torsten Schulz (local)
e3df88bea0 Enhance getCumulativeTaxPercentWithExemptions function in FalukantService to first retrieve the character associated with the userId, ensuring accurate filtering of political offices by characterId for regional tax calculations. 2025-12-20 11:09:03 +01:00
Torsten Schulz (local)
c69a414f78 Fix cumulative tax calculation in FalukantService by using falukantUser.id instead of user.id for accurate regional tax assessments. 2025-12-20 11:04:21 +01:00
Torsten Schulz (local)
d08022ab94 Merge branch 'main' of ssh://tsschulz.de:2222/torsten/yourpart3 2025-12-20 10:54:58 +01:00
Torsten Schulz (local)
66e6fab663 Refactor getCumulativeTaxPercentWithExemptions function in falukantService.js to filter political offices by userId through the FalukantCharacter model. Update query structure to enhance clarity and ensure accurate data retrieval for regional tax calculations. 2025-12-20 10:48:56 +01:00
4da572822e Merge pull request 'Update dependency nodemon to v3.1.11' (#3) from renovate/nodemon-3.x-lockfile into main
Reviewed-on: #3
2025-12-19 16:16:21 +01:00
ee23bb3ba3 Merge pull request 'Replace dependency npm-run-all with npm-run-all2 ^5.0.0' (#2) from renovate/npm-run-all-replacement into main
Reviewed-on: #2
2025-12-19 16:16:12 +01:00
d002e340dd Update dependency nodemon to v3.1.11 2025-12-19 16:14:33 +01:00
0e1d87ddab Replace dependency npm-run-all with npm-run-all2 ^5.0.0 2025-12-19 16:14:29 +01:00
2a4928c1b6 Merge pull request 'Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2025-12-19 16:07:26 +01:00
efe2bd57ab Add renovate.json 2025-12-19 16:00:42 +01:00
Torsten Schulz (local)
a0aa678e7d Implement logic to create tables without Foreign Key constraints in sequelize.js when referenced tables do not exist. Enhance error handling and logging to provide clear feedback during synchronization attempts, improving robustness in model management. 2025-12-19 08:37:40 +01:00
Torsten Schulz (local)
a1b6e6ab59 Enhance error handling in sequelize.js for Foreign Key Constraint Errors by adding logging for orphaned records and skipping problematic models during synchronization. Update syncDatabase.js to include cleanup logic for orphaned political_office entries, improving database integrity and user feedback during sync operations. 2025-12-19 08:34:04 +01:00
Torsten Schulz (local)
73acf1d1cd Refactor error handling in sequelize.js to skip model synchronization for cases with duplicate pg_description entries or multiple tables with the same name. Update logging to provide clearer feedback on sync failures and the reasons for skipping models, enhancing user understanding of potential issues. 2025-12-19 08:13:52 +01:00
Torsten Schulz (local)
48110e9a6f Improve error handling and logging for duplicate pg_description cleanup in sequelize.js. Update comments for clarity on permission requirements and provide detailed instructions for manual cleanup by database administrators. Enhance user feedback during synchronization attempts to address potential permission issues. 2025-12-19 07:56:07 +01:00
Torsten Schulz (local)
642e215c69 Refactor duplicate entry cleanup in sequelize.js by replacing DO $$ blocks with direct parameter substitution in SQL queries. This change enhances performance and security while maintaining the logic for cleaning up duplicate pg_description entries before and after model synchronization. 2025-12-19 07:53:34 +01:00
Torsten Schulz (local)
091b9ff70a Enhance model synchronization in sequelize.js by adding logic to clean up duplicate pg_description entries before and after sync attempts. Implement error handling for potential sync failures related to duplicate entries, improving robustness and clarity in foreign key management during model synchronization. 2025-12-18 17:53:24 +01:00
Torsten Schulz (local)
86f753c745 Refactor associations in models to include constraints: false, preventing automatic foreign key creation. Update sequelize.js to enhance foreign key management during model synchronization, ensuring associations are restored correctly after sync operations. 2025-12-18 17:44:17 +01:00
Torsten Schulz (local)
c28f8b1384 Enhance foreign key management in sequelize.js by refining schema handling and improving logging for foreign key removal during model synchronization. Add detailed console outputs for better visibility on foreign key operations and error handling. 2025-12-18 16:45:56 +01:00
Torsten Schulz (local)
9b36297171 Implement foreign key removal before model synchronization in sequelize.js to prevent conflicts during sync. Add error handling and logging for better visibility on foreign key management. 2025-12-18 16:39:34 +01:00
Torsten Schulz (local)
7beed235d7 Improve model synchronization in sequelize.js by temporarily removing associations to prevent automatic foreign key creation. Add logging for association management during the sync process, ensuring clarity in model handling. 2025-12-18 16:36:26 +01:00
Torsten Schulz (local)
a0206dc8cb Add logging for model synchronization and cache handling in syncDatabase.js
Enhance sequelize.js by adding a console log to indicate when models are being synced without constraints. Update syncDatabase.js to include important notes on caching issues with Node.js ES-Modules and log the model loading process during deployment synchronization.
2025-12-18 16:34:10 +01:00
Torsten Schulz (local)
bf0eed3b03 Update model synchronization in sequelize.js to prevent automatic foreign key creation by adding constraints: false, ensuring foreign keys are managed through migrations only. 2025-12-18 16:14:53 +01:00
Torsten Schulz (local)
c8072b8052 Refactor multiple models to remove foreign key references while maintaining required fields, enhancing data integrity and simplifying model definitions. 2025-12-18 16:08:30 +01:00
Torsten Schulz (local)
c66fbf1a62 Enhance syncDatabase function to include cleanup for orphaned child_relation entries with invalid father_character_id, mother_character_id, or child_character_id references, improving data integrity and logging consistency. 2025-12-18 15:59:35 +01:00
Torsten Schulz (local)
e13a711a60 Refactor user_house model to remove default values for houseTypeId and userId fields, and enhance syncDatabase function to include cleanup for orphaned user_house entries with invalid house_type_id or user_id references, improving data integrity and logging. 2025-12-18 15:57:39 +01:00
Torsten Schulz (local)
346a326bfd Enhance syncDatabase function to include cleanup for orphaned promotional_gift entries, removing invalid sender and recipient character references, and improve logging for orphaned entry detection. 2025-12-18 15:49:34 +01:00
Torsten Schulz (local)
addb8e9a6d Refactor Notification model to remove VIRTUAL field definition for characterName and implement a getter method for improved data handling and synchronization. 2025-12-18 15:43:54 +01:00
Torsten Schulz (local)
ea8b9e661d Refactor VIRTUAL field detection logic in sequelize.js to improve accuracy and add special handling for Notification model's characterName field, addressing a Sequelize bug related to field mapping. 2025-12-18 15:37:52 +01:00
Torsten Schulz (local)
339ae844e9 Enhance VIRTUAL field detection in sequelize.js by implementing multiple identification methods, ensuring accurate model synchronization and preventing unintended field removals. 2025-12-18 15:34:26 +01:00
Torsten Schulz (local)
a0a7e81927 Add socket notification for status bar updates in FalukantService and enhance model sync by handling VIRTUAL fields in sequelize.js 2025-12-18 15:25:24 +01:00
Torsten Schulz (local)
31c23a0c40 Refactor syncDatabase function to improve orphaned entry cleanup for knowledge and notification, ensuring data integrity and consistent logging. 2025-12-18 15:20:09 +01:00
Torsten Schulz (local)
c1f22246ea Add cleanup for orphaned notification entries in syncDatabase functions to remove invalid user_id references, improving data integrity and logging consistency. 2025-12-18 15:18:21 +01:00
Torsten Schulz (local)
0a1388bf06 Add cleanup for orphaned knowledge entries in syncDatabase functions to remove invalid character_id and product_id references, enhancing data integrity and logging. 2025-12-18 15:17:01 +01:00
Torsten Schulz (local)
1a69b83983 Refactor stock cleanup logic in syncDatabase functions to remove orphaned stock entries with invalid branch_id and streamline logging for orphaned entries. 2025-12-18 15:13:24 +01:00
Torsten Schulz (local)
63f9443b77 Implement cleanup of orphaned user_param_visibility entries before schema updates in syncDatabase functions 2025-12-18 15:11:50 +01:00
Torsten Schulz (local)
6a9b2b8d1d Add index on (user_id, shown) in notification table to optimize markNotificationsShown queries and prevent deadlocks. Implement transaction handling in markNotificationsShown method for atomic updates. 2025-12-18 15:04:37 +01:00
Torsten Schulz (local)
8e1e0968ae Refactor product model by removing unused sellCostMinNeutral and sellCostMaxNeutral fields, and simplify product insertion logic in initialization script. 2025-12-18 14:41:56 +01:00
Torsten Schulz (local)
a486292880 Activate pgcrypto extension for digest() function in database initialization and migration scripts 2025-12-18 14:11:15 +01:00
Torsten Schulz (local)
ee4b0ee7c2 Füge Spalte product_quality zur Tabelle stock hinzu und erstelle Migration für weather_type_id in production 2025-12-16 13:00:29 +01:00
Torsten Schulz (local)
43d86cce18 Implement tax handling for branches by adding tax percent to regions, updating product sell costs, and enhancing UI for tax summaries in BranchView 2025-12-09 16:16:08 +01:00
Torsten Schulz (local)
25d7c70058 Enhance transport mode handling by adding localized labels and updating related components in MapRegionsView and BranchView 2025-12-09 11:53:56 +01:00
Torsten Schulz (local)
71c62cf5e8 Enhance vehicle speed display by adding localized labels in DirectorInfo, SaleSection, and BranchView components 2025-12-09 11:45:35 +01:00
Torsten Schulz (local)
a7350282ee Enhance parameter extraction in MessagesDialog by merging nested parameters for improved notification handling 2025-12-09 00:12:05 +01:00
Torsten Schulz (local)
676629bd8d Enhance notification enrichment by recursively collecting character IDs and attaching character names 2025-12-09 00:06:09 +01:00
Torsten Schulz (local)
1892877b11 Enhance notification handling by enriching notifications with character names 2025-12-08 23:55:50 +01:00
Torsten Schulz (local)
be218aabf7 Add character_name field and trigger for notifications in Falukant module 2025-12-08 23:37:07 +01:00
Torsten Schulz (local)
856f7d56bf Enhance parameter extraction for notifications in MessagesDialog component 2025-12-08 16:12:05 +01:00
Torsten Schulz (local)
000ebbdc2b Enhance currency formatting in MoneyHistoryView component 2025-12-08 15:35:17 +01:00
Torsten Schulz (local)
791314bef2 Enhance notification display and localization in MessagesDialog component
- Updated the MessagesDialog component to display notifications with titles and descriptions, improving clarity and user experience.
- Enhanced the formatBody method to support new notification structures, including extraction and formatting of parameters for better message presentation.
- Added a new formatParams method to handle various parameter types, ensuring accurate representation of values in notifications.
- Updated localization files in both German and English to include structured titles and descriptions for random events, enriching the user experience with detailed information.
2025-12-08 14:42:17 +01:00
Torsten Schulz (local)
bcb0b01324 Enhance child management features in Falukant module
- Added new translations for gender, baptism status, and child details in both German and English localization files, improving user experience.
- Integrated ChildDetailsDialog component into FamilyView for displaying detailed information about children.
- Updated the showChildDetails method to utilize the new dialog for better user interaction.
- Modified button styles for improved visual feedback when setting heirs.
2025-12-08 13:30:11 +01:00
Torsten Schulz (local)
03e3a21a25 Add heir management functionality in Falukant module
- Implemented setHeir method in FalukantService to designate a child as heir, including validation checks for user and child relationships.
- Updated FalukantController to expose the setHeir endpoint, allowing users to set heirs via the API.
- Enhanced FalukantRouter with a new route for setting heirs.
- Modified FamilyView component to include UI elements for setting heirs, with success and error feedback.
- Updated localization files in both German and English to include new translations related to heir management, improving user experience.
2025-12-08 13:22:43 +01:00
Torsten Schulz (local)
e97a2a62c9 Enhance weather data handling in FalukantService and update localization files
- Modified the FalukantService to explicitly load weather data for all regions, ensuring accurate weather information is associated with branches.
- Updated the return logic to utilize the newly loaded weather data, improving data accuracy in branch responses.
- Added new random event messages in both German and English localization files, enhancing user experience with richer event descriptions.
2025-12-08 11:54:10 +01:00
Torsten Schulz (local)
814f972287 Update branch selection logic in BranchView component
- Enhanced the onBranchSelected method to reload branches for updated weather information and reset the selected branch after reloading.
- Improved user experience by ensuring the correct branch is selected post-refresh, maintaining data accuracy and consistency.
2025-12-08 11:34:50 +01:00
Torsten Schulz (local)
274c2a3292 Add income update success message in DirectorInfo component
- Implemented a success message display for income updates in the DirectorInfo component, enhancing user feedback after successful updates.
- Added a timeout to automatically hide the success message after 3 seconds.
- Updated localization files to include new translations for income-related messages in both German and English, improving user experience for multilingual users.
2025-12-08 11:30:31 +01:00
Torsten Schulz (local)
4dbcebfab8 Add handling for transport removal events in BranchView component
- Implemented logic to update vehicle and inventory data when a transport is removed, ensuring real-time synchronization with the selected branch.
- Enhanced the component to refresh relevant sections (vehicles, inventory, storage) based on the transport removal event, improving user experience and data accuracy.
2025-12-08 09:36:18 +01:00
Torsten Schulz (local)
fadc301d41 Add bulk vehicle repair functionality in Falukant module
- Implemented a new repairAllVehicles method in FalukantService to handle the repair of multiple vehicles at once, including cost calculation and precondition checks.
- Updated FalukantController to expose the repairAllVehicles endpoint, allowing users to initiate bulk repairs via the API.
- Enhanced FalukantRouter to include a new route for bulk vehicle repairs.
- Modified BranchView component to add UI elements for repairing all vehicles, including a dialog for confirmation and displaying repair details.
- Updated German localization files to include translations related to bulk vehicle repair actions, improving user experience for German-speaking users.
2025-12-08 08:36:21 +01:00
Torsten Schulz (local)
b1d29f2083 Enhance nobility ID validation in FalukantService
- Added checks to ensure that provided nobility IDs are valid and exist in the database, improving error handling and user feedback.
- Updated logic to use loaded nobility objects when adding invited nobilities to a party, optimizing database interactions.
2025-12-05 20:49:12 +01:00
Torsten Schulz (local)
e756b3692d Refactor availability status logic in FalukantService
- Enhanced the logic for determining the availability status of vehicles based on the 'availableFrom' date.
- Added conditions to differentiate between 'building' and 'available' statuses, improving clarity and accuracy in status reporting.
- Updated comments for better understanding of the code flow.
2025-12-05 17:23:54 +01:00
Torsten Schulz (local)
74a3d59800 Add vehicle repair functionality in Falukant module
- Implemented a new repairVehicle method in FalukantService to handle vehicle repairs, including cost calculation and precondition checks.
- Updated FalukantController to expose the repairVehicle endpoint, allowing users to initiate repairs via the API.
- Enhanced FalukantRouter to include a new route for vehicle repairs.
- Modified BranchView component to add UI elements for repairing vehicles, including a dialog for repair confirmation and displaying repair details.
- Updated German localization files to include translations related to vehicle repair actions, improving user experience for German-speaking users.
2025-12-05 14:40:55 +01:00
Torsten Schulz (local)
0544a3dfde Add transport and inventory update handling in BranchView component
- Implemented socket event listeners for 'transport_arrived' and 'inventory_updated' to manage real-time updates in the BranchView component.
- Enhanced event handling logic to refresh vehicle and inventory data based on the selected branch, improving user experience and data accuracy.
- Updated the component to ensure proper cleanup of socket listeners on component destruction, maintaining optimal performance.
2025-12-05 14:13:14 +01:00
Torsten Schulz (local)
656c821986 Enhance SaleSection component to group and display transport data
- Updated SaleSection.vue to group running transports by relevant attributes, improving data organization and readability.
- Added a new computed property to calculate vehicle counts and total quantities for grouped transports.
- Introduced a new column in the UI to display the count of vehicles associated with each transport group.
- Updated German localization file to include translation for 'runningVehicleCount', enhancing user experience for German-speaking users.
2025-12-05 13:12:24 +01:00
Torsten Schulz (local)
865ef81012 Enhance FalukantService and UI components for improved product handling
- Updated FalukantService to allow optional inclusion of productType in queries, enhancing flexibility in data retrieval.
- Modified SaleSection.vue to conditionally display product information and size, improving user experience by handling cases with no product.
- Added new German translation for 'runningNoProduct' to enhance localization support for users.
2025-12-05 13:07:31 +01:00
Torsten Schulz (local)
5ad27a87e5 Enhance vehicle transport functionality in FalukantService and update UI components
- Modified the createTransport method in FalukantService to support optional vehicleIds, allowing for more flexible vehicle selection.
- Implemented logic to ensure that either specific vehicleIds or a vehicleTypeId must be provided, improving error handling for vehicle availability.
- Updated the BranchView component to include new UI elements for sending vehicles, including buttons for sending single or multiple vehicles of the same type.
- Added a modal dialog for selecting target branches when sending vehicles, enhancing user experience and streamlining transport operations.
- Updated German localization files to include new translations related to vehicle actions and transport functionalities.
2025-12-05 12:49:37 +01:00
Torsten Schulz (local)
085b851925 Add German translation for 'townhouse' in falukant.json
- Updated the German localization file to include the translation for 'townhouse' as 'Stadthaus'.
- This addition enhances the application's multilingual support and improves user experience for German-speaking users.
2025-12-05 11:42:41 +01:00
Torsten Schulz (local)
98dea7dd39 Implement empty transport feature in DirectorInfo component
- Added functionality to allow directors to initiate empty transports without products, enhancing logistics management.
- Introduced a new transport form in the DirectorInfo component, enabling selection of vehicle types and target branches.
- Updated the i18n localization files to include new translations for the empty transport feature.
- Enhanced the BranchView to pass vehicle and branch data to the DirectorInfo component, ensuring proper functionality.
- This update aims to improve user experience and streamline transport operations within the application.
2025-12-04 14:48:55 +01:00
Torsten Schulz (local)
e5ef334f7c Update FalukantService and PoliticsView to enhance election data handling
- Modified the FalukantService to use getOpenPolitics instead of getElections for retrieving accessible elections, improving alignment with frontend data display.
- Updated the PoliticsView to handle the response from the application submission more effectively, ensuring that already applied positions remain pre-selected after submission.
- These changes aim to streamline the election data flow and enhance user experience in the application process.
2025-12-03 17:19:13 +01:00
Torsten Schulz (local)
d6ea09b3e2 Enhance RevenueSection UI and streamline price loading logic
- Updated the display of city prices in the RevenueSection component to include both city names and formatted price values, improving user experience.
- Removed unnecessary console logs from the loadPricesForAllProducts method to clean up the code and reduce clutter, while maintaining essential functionality.
- Simplified the getBetterPrices method by eliminating redundant logging, enhancing code clarity and performance.
2025-12-03 16:30:10 +01:00
Torsten Schulz (local)
a51b8a1ff6 Fix 2025-12-03 16:29:56 +01:00
Torsten Schulz (local)
3c885b6ab9 Add detailed debug logging in loadPricesForAllProducts method of RevenueSection
- Enhanced the loadPricesForAllProducts method with additional console logs to track the loading process of product prices, including the current region ID and the number of products being processed.
- Improved visibility into the state of betterPricesMap after updates and provided detailed logs for each product's price loading, facilitating easier debugging and monitoring of price retrieval.
- Aims to enhance traceability and provide clearer insights into the price handling process within the RevenueSection component.
2025-12-03 16:22:08 +01:00
Torsten Schulz (local)
6b3b30108b Refactor betterPricesMap updates in RevenueSection for Vue 3 reactivity
- Updated the handling of betterPricesMap to create a new object for state updates, ensuring reactivity in Vue 3.
- This change replaces direct assignments with spread operator syntax to maintain the integrity of the reactive system.
- Aims to improve performance and align with Vue 3 best practices for state management.
2025-12-03 16:15:01 +01:00
Torsten Schulz (local)
7fab23d22b Refactor betterPricesMap handling in RevenueSection for Vue 3 compatibility
- Removed the use of $set for updating betterPricesMap, leveraging direct assignment instead, which is now the standard in Vue 3.
- Simplified the getBetterPrices method by eliminating unnecessary logging, enhancing code clarity while maintaining functionality.
- These changes aim to improve performance and align with Vue 3 best practices for state management.
2025-12-03 16:03:06 +01:00
Torsten Schulz (local)
def88f6486 Add debug logging in RevenueSection for better price retrieval tracking
- Introduced console logs to track the number of better prices received for each product and the state of the betterPricesMap after updates.
- Enhanced the getBetterPrices method with logging to provide visibility into the prices being returned, improving traceability during price evaluations.
- These changes aim to facilitate debugging and provide clearer insights into the price handling process within the RevenueSection component.
2025-12-03 15:59:15 +01:00
Torsten Schulz (local)
1797ae3e58 Remove debug logging from getProductPricesInCities method in FalukantService
- Eliminated console logs that tracked various parameters and results within the getProductPricesInCities method, streamlining the code and reducing output clutter.
- This change aims to enhance code readability and maintain focus on essential functionality while maintaining the integrity of the price calculation process.
2025-12-03 15:55:30 +01:00
Torsten Schulz (local)
f768ba3b27 Add debug logging for priceInCity in getProductPricesInCities method of FalukantService
- Introduced a console log to capture the values of priceInCity, currentPrice, and PRICE_TOLERANCE, enhancing visibility into the price comparison process.
- This addition aims to improve traceability and facilitate debugging during price evaluations, building on previous logging enhancements.
2025-12-03 15:39:57 +01:00
Torsten Schulz (local)
b3e48a0b06 Refine price comparison logic in getProductPricesInCities method of FalukantService
- Introduced a small tolerance (0.01) for rounding errors in the price comparison, allowing for more accurate evaluations when determining if a city's price exceeds the current price.
- This change enhances the robustness of price calculations by accommodating potential floating-point inaccuracies.
2025-12-03 15:35:19 +01:00
Torsten Schulz (local)
3f56939421 Add detailed debug logging in getProductPricesInCities method of FalukantService
- Introduced console logs to trace the execution flow and key variables in the getProductPricesInCities method, enhancing visibility into product price calculations.
- Logged parameters such as productId, currentPrice, and currentRegionId at the start of the method.
- Added logs for the number of cities and town worth entries found, as well as details when skipping the current city and adding results, improving traceability during price evaluations.
- This update aims to facilitate debugging and performance monitoring by providing comprehensive insights into the pricing logic.
2025-12-03 13:39:09 +01:00
Torsten Schulz (local)
87c720c3fe Refactor RevenueSection to utilize a betterPricesMap for improved price handling
- Replaced direct product.betterPrices usage with a betterPricesMap to store prices separately, enhancing data management.
- Updated computed properties and methods to clear betterPricesMap when product list or region changes, ensuring accurate price loading.
- Introduced getBetterPrices method for cleaner access to price data, improving code readability and maintainability.
2025-12-03 13:32:02 +01:00
Torsten Schulz (local)
90fbcaf31d Refactor and remove debug logging in FalukantService and RevenueSection for cleaner code
- Eliminated console logs in the getProductPricesInCities method of FalukantService to streamline the price calculation process and reduce clutter in the output.
- Removed unnecessary debug logs in RevenueSection related to loading product prices, enhancing performance and focusing on essential functionality.
- Improved overall code readability by reducing logging noise while maintaining necessary functionality.
2025-12-03 11:35:13 +01:00
Torsten Schulz (local)
56c3569b68 Refactor debug logging in FalukantService for improved clarity and consistency
- Removed specific debug logs for the carrot product and replaced them with generalized logs for all products, enhancing the readability of the logging output.
- Updated console logs to provide clearer information about the processing of cities and the results returned, improving traceability during price calculations.
- Ensured that all relevant details are logged consistently, aiding in debugging and performance monitoring across different product types.
2025-12-03 11:32:06 +01:00
Torsten Schulz (local)
e2969c1837 Enhance RevenueSection to conditionally load product prices based on currentRegionId
- Updated watchers in RevenueSection to ensure product prices are only loaded when currentRegionId is not null.
- Added a check in loadPricesForAllProducts to skip execution if currentRegionId is null or undefined, improving performance and preventing unnecessary calls.
- Enhanced overall logic to ensure accurate price loading based on the selected region, contributing to a better user experience.
2025-12-03 09:19:46 +01:00
Torsten Schulz (local)
fe14c7b9f5 Add debug logging for product price retrieval in FalukantService and RevenueSection
- Introduced console logs in FalukantService to trace the parameters used in the getProductPricesInCities method, enhancing visibility into the product price retrieval process.
- Added logging in RevenueSection to capture the loading process and received better prices for products, improving traceability and debugging capabilities during price loading operations.
2025-12-03 08:58:07 +01:00
Torsten Schulz (local)
5d01b24c2d Add debug logging for carrot product pricing in FalukantService
- Introduced console logs to trace the price calculation process specifically for the carrot product (productId: 3).
- Enhanced visibility into the evaluation of cities and the resulting prices, aiding in debugging and performance monitoring.
- Logged details when skipping the current city, calculating prices, and adding cities to the results, improving traceability of pricing logic.
2025-12-03 08:44:45 +01:00
Torsten Schulz (local)
4eeb5021ee Enhance product price retrieval by including currentRegionId in FalukantController and FalukantService
- Updated the FalukantController to accept currentRegionId as a parameter for fetching product prices in cities.
- Modified the FalukantService to incorporate currentRegionId in the price calculation logic, allowing exclusion of the current region from results.
- Adjusted frontend components to pass currentRegionId, improving the accuracy of price comparisons and user experience.
2025-12-03 08:39:30 +01:00
Torsten Schulz (local)
6ec62af606 Add debug logging to FalukantService and RevenueSection for better price tracking
- Introduced console logs in FalukantService to trace product price calculations and city evaluations, aiding in debugging and performance monitoring.
- Added logging in RevenueSection to capture the loading process and received better prices for products, enhancing visibility into price retrieval operations.
2025-12-03 08:30:59 +01:00
Torsten Schulz (local)
3d6fdc65d2 Refine price comparison logic in FalukantService to include a tolerance for rounding errors
- Updated the price comparison condition to account for a small tolerance (0.001) when determining if the calculated price in a city exceeds the current price, improving accuracy in pricing evaluations.
2025-12-02 15:57:29 +01:00
Torsten Schulz (local)
956418f5f3 Enhance weather model and service logic; improve money history translation handling
- Added primary key to the Weather model for better data integrity.
- Updated FalukantService to include specific weather attributes in queries, enhancing data retrieval.
- Refactored money history view to utilize a dedicated translation method for improved localization handling.
2025-12-02 14:05:25 +01:00
Torsten Schulz (local)
e57de7f983 Fix typo in healthDrunkOfLife method and enhance health change logic in FalukantService; refactor health measures localization structure in English and German JSON files for better organization. 2025-12-02 13:05:39 +01:00
Torsten Schulz (local)
08e2c87de8 Enhance branch selection with weather information and localization updates
- Updated FalukantService to include weather data in branch retrieval, enhancing user context.
- Modified BranchSelection component to display current weather for selected branches, improving user experience.
- Added weather translations in both English and German localization files for better accessibility.
- Improved styling for weather information display in the frontend.
2025-12-02 12:53:02 +01:00
Torsten Schulz (local)
ba1a12402d Add product weather effects and regional pricing enhancements
- Introduced a new endpoint in FalukantController to retrieve product prices based on region and product ID.
- Implemented logic in FalukantService to calculate product prices considering user knowledge and regional factors.
- Added weather-related data models and associations to enhance product pricing accuracy based on weather conditions.
- Updated frontend components to cache and display regional product prices effectively, improving user experience.
2025-12-02 09:55:08 +01:00
Torsten Schulz (local)
39716b1f40 Add regional pricing calculation for products in FalukantService
- Introduced a new function `calcRegionalSellPrice` to compute product prices based on regional worth percentages.
- Updated existing methods to utilize the new pricing logic, ensuring revenue calculations reflect regional variations.
- Integrated retrieval of `TownProductWorth` data to enhance pricing accuracy across different regions.
2025-12-02 08:44:53 +01:00
Torsten Schulz (local)
adc7132404 Add product price retrieval feature in cities
- Implemented a new endpoint in FalukantController to fetch product prices in various cities based on product ID and current price.
- Developed the corresponding service method in FalukantService to calculate and return prices, considering user knowledge and city branches.
- Updated frontend components (RevenueSection and SaleSection) to display better prices for products, including loading logic and UI enhancements for price visibility.
- Added styling for price indicators based on branch types to improve user experience.
2025-12-01 16:42:54 +01:00
Torsten Schulz (local)
8c8841705c Implement daemon socket listener management in BranchView.vue
- Added a watcher for the daemon socket to properly register and unregister message event listeners on socket changes.
- Simplified the event listener setup for handling daemon messages, improving code clarity and maintainability.
- Ensured that listeners are removed during component unmount to prevent memory leaks.
2025-12-01 14:06:18 +01:00
Torsten Schulz (local)
f7fdd8ab08 Refactor localization structure for production notifications in English and German
- Updated the localization files to nest the "overproduction" notification under a "production" key for better organization and clarity.
- Ensured consistency in translation structure across both English and German localization files.
2025-12-01 11:51:37 +01:00
Torsten Schulz (local)
5807c6f3d3 Update daemon socket configuration and fallback logic in frontend scripts
- Changed the default value for `VITE_DAEMON_SOCKET` in `deploy-frontend.sh` and `update-frontend.sh` to connect directly to port 4551 instead of using the Apache proxy.
- Updated fallback logic in `frontend/src/store/index.js` to reflect the new direct connection to the daemon on port 4551, enhancing connection reliability.
2025-12-01 11:46:50 +01:00
Torsten Schulz (local)
7e0691eea3 Enhance message formatting and localization handling in MessagesDialog.vue
- Updated the formatBody method to support JSON formatted translation keys and improve key normalization for i18n.
- Ensured that keys are correctly prefixed with the "falukant.notifications." namespace when necessary, enhancing translation accuracy.
2025-12-01 11:26:46 +01:00
Torsten Schulz (local)
17d4d21620 Add new daemon start script and update localization for director salary
- Introduced a new script `start-daemon` in `package.json` for running the daemon server.
- Added translations for "director payed out" in both English and German localization files to enhance user notifications.
2025-12-01 10:06:06 +01:00
Torsten Schulz (local)
d19feb8bc1 Update daemon socket URL and enhance message rendering in frontend
- Changed the default value for `VITE_DAEMON_SOCKET` in `deploy-frontend.sh` and `update-frontend.sh` to use the `/ws/` path.
- Updated the message rendering logic in `MessagesDialog.vue` to utilize a new `formatBody` method for improved translation handling.
- Added a new translation for "overproduction" in both English and German localization files.
2025-12-01 09:47:16 +01:00
Torsten Schulz (local)
ab1e4bec60 Update localization for notifications in English and German
- Added new notification translations for election creation in both `falukant.json` files.
- Updated the message rendering in `MessagesDialog.vue` to include the new translation structure.
2025-12-01 09:32:59 +01:00
Torsten Schulz (local)
672cec9c2a Add localization updates for money history in English and German 2025-12-01 09:28:44 +01:00
Torsten Schulz (local)
c3ea7eecc2 Update dependencies and refactor authentication logic
- Replaced `bcrypt` with `bcryptjs` for compatibility in `authService.js` and `settingsService.js`.
- Updated package versions in `package.json` and `package-lock.json`, including `multer`, `nodemailer`, and others.
- Added storage management features in the frontend, including free storage calculation and localization updates for new terms in `falukant.json` files.
2025-11-26 18:14:36 +01:00
Torsten Schulz (local)
608e62c2bd Implement cooldown feature for nobility advancement
- Added logic in FalukantService to calculate the next available advancement date based on the user's last advancement.
- Updated the frontend to display a cooldown message indicating when the user can next advance in nobility.
- Enhanced the NobilityView component to handle and format the next advancement date appropriately.
2025-11-26 17:23:54 +01:00
Torsten Schulz (local)
c1b69389c6 Add lastNobilityAdvanceAt field and update logic in FalukantService
- Introduced a new field `lastNobilityAdvanceAt` in the FalukantUser model to track the last time a user advanced in nobility.
- Updated the `FalukantService` to enforce a one-week cooldown between nobility advancements, throwing an error if the user attempts to advance too soon.
- Ensured the `lastNobilityAdvanceAt` field is updated with the current date upon a successful nobility advancement.
2025-11-26 17:17:37 +01:00
Torsten (PC)
182f38597c update-funktion verbessert 2025-11-26 17:16:30 +01:00
Torsten Schulz (local)
06ea259dc9 Add Falukant region and transport management features
- Implemented new endpoints in AdminController for managing Falukant regions, including fetching, updating, and deleting region distances.
- Enhanced the FalukantService with methods for retrieving region distances and handling upsert operations.
- Updated the router to expose new routes for region management and transport creation.
- Introduced a transport management interface in the frontend, allowing users to create and manage transports between branches.
- Added localization for new transport-related terms and improved the vehicle management interface to include transport options.
- Enhanced the database initialization logic to support new region and transport models.
2025-11-26 16:44:27 +01:00
Torsten Schulz (local)
29dd7ec80c Refactor daemon connection logic and enhance error handling
- Simplified fallback logic for daemon URL generation, removing hardcoded values and using dynamic protocol and hostname.
- Added detailed error messages for common WebSocket connection issues, improving debugging capabilities.
- Updated reconnection warning messages to guide users on potential configuration issues with the daemon server.
2025-11-24 20:28:11 +01:00
Torsten Schulz (local)
3f043fc315 Add vehicle management features in Falukant
- Introduced vehicle types and transport management in the backend, including new models and associations for vehicles and transports.
- Implemented service methods to retrieve vehicle types and handle vehicle purchases, ensuring user validation and transaction management.
- Updated the FalukantController and router to expose new endpoints for fetching vehicle types and buying vehicles.
- Enhanced the frontend with a new transport tab in BranchView, allowing users to buy vehicles, and added localization for vehicle-related terms.
- Included initialization logic for vehicle types in the database setup.
2025-11-24 20:15:45 +01:00
Torsten Schulz (local)
5ed27e5a6a Refactor navigation and enhance director information display
- Removed the directors section from the navigation menu for a cleaner interface.
- Updated the FalukantService to include additional attributes for directors, such as knowledges and region.
- Enhanced the DirectorInfo component to display detailed information, including knowledge and income management features.
- Implemented tab navigation in BranchView for better organization of director, inventory, production, and storage sections.
- Updated localization files to reflect changes in navigation and tab labels.
2025-11-24 16:38:36 +01:00
Torsten Schulz (local)
23725c20ee Enhance mood change calculation in FalukantService
- Updated the mood change calculation to include a random bonus between 0 and 7 points, improving variability in user experience.
- Refactored the calculation logic for clarity, separating the base change value from the random bonus.
2025-11-24 15:51:27 +01:00
Torsten Schulz (local)
29b6db7ee9 Update dropdown positioning in FormattedDropdown component for improved visibility
- Changed the dropdown list positioning from normal document flow to absolute positioning, ensuring the list is reliably visible when opened.
2025-11-24 15:39:44 +01:00
Torsten Schulz (local)
6e7165fe7f Add console log to toggleDropdown method in FormattedDropdown component for debugging 2025-11-24 15:33:55 +01:00
Torsten Schulz (local)
43131250ed Fix dropdown toggle method in FormattedDropdown component to ensure proper function call 2025-11-24 15:26:44 +01:00
Torsten Schulz (local)
c3beb029e5 Refactor FormattedDropdown and enhance BranchView functionality
- Updated the FormattedDropdown component to use normal document flow for the dropdown list, ensuring visibility when opened.
- Enhanced the createBranch method in BranchView to automatically select the most recently created branch after a new branch is added, improving user experience.
2025-11-24 15:19:35 +01:00
Torsten Schulz (local)
9f10ac9e96 Enhance BranchSelection component to force re-render on branch list change
- Added a computed property `branchesKey` to generate a unique key based on branch IDs, ensuring the dropdown re-renders when the branch list updates.
- Updated the FormattedDropdown component to utilize this key for improved responsiveness to data changes.
2025-11-24 13:45:04 +01:00
Torsten Schulz (local)
d36901aa2b Refactor tab change logic in PoliticsView to simplify loading conditions
- Updated the onTabChange method to remove unnecessary checks for existing data before loading current positions, open politics, and elections.
- This change enhances the clarity of the method and ensures that data is always loaded when the respective tab is selected.
2025-11-24 12:24:31 +01:00
Torsten Schulz (local)
4510aa3d14 Implement politics overview feature in FalukantService and update UI
- Added a new method `getPoliticsOverview` in FalukantService to retrieve currently held offices, including office holders and term end dates.
- Enhanced the PoliticsView component to display the term end dates for current offices.
- Updated localization files to include a new message for applying to selected positions.
- Improved the handling of already applied positions in the open politics section, pre-selecting checkboxes accordingly.
2025-11-24 11:50:21 +01:00
Torsten Schulz (local)
3b8736acd7 Enhance WebSocketLogDialog to display enriched user information
- Updated the WebSocketLogDialog to use enriched log entries with resolved usernames for connection and target users.
- Implemented batch retrieval of user information from the API to improve user display in logs.
- Added error handling for user data fetching and fallback logic for missing usernames.
2025-11-22 13:32:44 +01:00
Torsten Schulz (local)
735075d1bd Add WebSocket Log feature to Services Status View
- Introduced a WebSocket Log section in the Services Status View, allowing users to view real-time logs.
- Updated localization files for both German and English to include WebSocket Log messages.
- Enhanced the UI with a button to open the WebSocket Log dialog, improving user interaction and monitoring capabilities.
2025-11-22 13:21:13 +01:00
Torsten Schulz (local)
dc7001a80c Implement batch user retrieval in AdminController and update routes
- Added a new method `getUsers` in AdminController to handle batch retrieval of user information based on hashed IDs.
- Updated adminRouter to include a new route for batch user retrieval.
- Enhanced AdminService with a method to fetch user details by hashed IDs, ensuring proper access control.
- Updated localization files to include the new "username" field for user connections in both German and English.
- Modified ServicesStatusView to utilize the new batch user retrieval for displaying usernames alongside connection counts.
2025-11-21 23:49:05 +01:00
Torsten Schulz (local)
8a9acf6c4a Refactor ServicesStatusView to handle daemon response structure
- Updated the handling of daemon responses to accommodate a new structure that includes user connection counts.
- Transformed the users object into an array for easier template rendering.
- Improved error handling for JSON parsing and daemon message processing.
2025-11-21 23:45:29 +01:00
Torsten Schulz (local)
5ca017950e Remove Google Chrome RPM package file 2025-11-20 15:52:04 +01:00
Torsten Schulz (local)
eadec50e30 Feature: Add Services Status page and update navigation
- Introduced a new Services Status page to monitor the status of Backend, Chat, and Daemon services.
- Updated navigation structure to include the new Services Status link for main admin users.
- Added German and English localization for the Services Status page, including titles, descriptions, and status messages.
2025-11-20 15:49:08 +01:00
Torsten Schulz (local)
e7f5918013 Enhance Vite configuration to load environment variables
- Refactored Vite configuration to load environment variables explicitly based on the current mode
- Added support for additional environment variables: VITE_DAEMON_SOCKET, VITE_API_BASE_URL, VITE_CHAT_WS_URL, and VITE_SOCKET_IO_URL
- Improved clarity and maintainability of the configuration structure
2025-11-18 08:59:20 +01:00
Torsten Schulz (local)
27b675cb19 Refactor daemon URL configuration and enhance logging
- Improved fallback logic for daemon URL based on hostname and environment
- Added detailed logging for daemon connection status and environment settings
- Streamlined handling of environment variables for better clarity
2025-11-18 08:50:25 +01:00
Torsten Schulz (local)
016a37c116 Refactor daemon connection logic and enhance logging
- Improved handling of daemon URL configuration based on environment variables
- Added detailed logging for daemon connection status and environment settings
- Streamlined fallback logic for local development and production environments
2025-11-18 08:37:02 +01:00
Torsten Schulz (local)
d8b1efc3ca Enhance StatusBar and daemon connection management
- Added image preloading for quick access in StatusBar component
- Implemented a watcher to reload images when the menu changes
- Introduced a delay before sending 'setUserId' to ensure daemon readiness
- Improved logging for WebSocket close events and errors
2025-11-17 16:19:43 +01:00
Torsten Schulz (local)
d13fe19198 Fix: Enhance daemon connection management and retry logic
- Clear socket reference on connection close and error
- Ensure reconnection attempts only occur if the user is logged in
- Improved logging for reconnection attempts and retry count
- Added maximum retry limit with extended wait time after reaching it
2025-11-16 11:33:20 +01:00
Torsten Schulz (local)
762a2e9cf0 Fix: Improve daemon connection handling and retry logic
- Reset daemon connection state on successful connection and errors
- Clear retry timer when connection is established
- Enhanced retry logic to prevent multiple simultaneous connection attempts
- Improved logging for daemon reconnection attempts
2025-10-31 16:24:35 +01:00
Torsten Schulz (local)
44a2c525e7 Fix: Restore original avatar images
- Avatar images should not be optimized as they are used for character display
- Restored original 1792x1024 resolution for proper character appearance
- Only small icons should be optimized, not character avatars
2025-10-24 23:22:22 +02:00
Torsten Schulz (local)
507b0275d3 Performance: Optimize all images and improve error handling
- Optimized Falukant shortmap icons: 1.4MB-2.9MB → 1.9KB-3.3KB (99%+ reduction)
- Optimized all large images: avatars, maps, passengers, products, etc.
- Improved error handling in getGifts method with better logging
- Fixed icon loading performance issues
- Maintained original design while dramatically improving load times
- Total space savings: ~100MB+
2025-10-24 23:18:18 +02:00
Torsten Schulz (local)
ccd8bfba0d Feature: Termine-Anzeige auf der Startseite
- Neue CSV-Datei backend/data/termine.csv für Termine-Speicherung
- Backend-Controller und Router für /api/termine Endpoint
- TermineWidget Component zur Anzeige von bevorstehenden Terminen
- Integration in LoggedInView (Startseite für eingeloggte User)
- Zeigt Datum, Titel, Beschreibung, Ort und Uhrzeit an
- Sortiert nach Datum, filtert automatisch vergangene Termine
2025-10-20 22:27:35 +02:00
Torsten Schulz (local)
47f5def67c Fix: Korrekter Tabellenname für UserRightType Model
- Ändere tableName von 'user_right_type' zu 'user_right'
- Die Tabelle heißt type.user_right, nicht type.user_right_type
- Behebt: Verwaltungsmenü wird nicht angezeigt für mainadmin
2025-10-20 21:33:12 +02:00
276 changed files with 28248 additions and 3391 deletions

5
.gitignore vendored
View File

@@ -17,3 +17,8 @@ frontend/dist
frontend/dist/* frontend/dist/*
frontedtree.txt frontedtree.txt
backend/dist/ backend/dist/
build
build/*
.vscode
.vscode/*
.clang-format

119
CMakeLists.txt Normal file
View File

@@ -0,0 +1,119 @@
cmake_minimum_required(VERSION 3.20)
project(YourPartDaemon VERSION 1.0 LANGUAGES CXX)
# C++ Standard and Compiler Settings
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Use best available GCC for C++23 support (OpenSUSE Tumbleweed)
# Try GCC 15 first (best C++23 support), then GCC 13, then system default
find_program(GCC15_CC gcc-15)
find_program(GCC15_CXX g++-15)
find_program(GCC13_CC gcc-13)
find_program(GCC13_CXX g++-13)
if(GCC15_CC AND GCC15_CXX)
set(CMAKE_C_COMPILER ${GCC15_CC})
set(CMAKE_CXX_COMPILER ${GCC15_CXX})
message(STATUS "Using GCC 15 for best C++23 support")
elseif(GCC13_CC AND GCC13_CXX)
set(CMAKE_C_COMPILER ${GCC13_CC})
set(CMAKE_CXX_COMPILER ${GCC13_CXX})
message(STATUS "Using GCC 13 for C++23 support")
else()
message(STATUS "Using system default compiler")
endif()
# Optimize for GCC 13 with C++23
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto=auto -O3 -march=native -mtune=native")
set(CMAKE_CXX_FLAGS_DEBUG "-O1 -g -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -march=native -mtune=native")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")
set(CMAKE_BUILD_TYPE Release)
# Include /usr/local if needed
list(APPEND CMAKE_PREFIX_PATH /usr/local)
# Find libwebsockets via pkg-config
find_package(PkgConfig REQUIRED)
pkg_check_modules(LWS REQUIRED libwebsockets)
# Find other dependencies
find_package(PostgreSQL REQUIRED)
find_package(Threads REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
# PostgreSQL C++ libpqxx
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBPQXX REQUIRED libpqxx)
# Project sources and headers
set(SOURCES
src/main.cpp
src/config.cpp
src/connection_pool.cpp
src/database.cpp
src/character_creation_worker.cpp
src/produce_worker.cpp
src/message_broker.cpp
src/websocket_server.cpp
src/stockagemanager.cpp
src/director_worker.cpp
src/valuerecalculationworker.cpp
src/usercharacterworker.cpp
src/houseworker.cpp
src/politics_worker.cpp
)
set(HEADERS
src/config.h
src/database.h
src/connection_pool.h
src/worker.h
src/character_creation_worker.h
src/produce_worker.h
src/message_broker.h
src/websocket_server.h
src/stockagemanager.h
src/director_worker.h
src/valuerecalculationworker.h
src/usercharacterworker.h
src/houseworker.h
src/politics_worker.h
)
# Define executable target
add_executable(yourpart-daemon ${SOURCES} ${HEADERS}
src/utils.h src/utils.cpp
src/underground_worker.h src/underground_worker.cpp)
# Include directories
target_include_directories(yourpart-daemon PRIVATE
${PostgreSQL_INCLUDE_DIRS}
${LIBPQXX_INCLUDE_DIRS}
${LWS_INCLUDE_DIRS}
)
# Find systemd
find_package(PkgConfig REQUIRED)
pkg_check_modules(SYSTEMD REQUIRED libsystemd)
# Link libraries
target_link_libraries(yourpart-daemon PRIVATE
${PostgreSQL_LIBRARIES}
Threads::Threads
z ssl crypto
${LIBPQXX_LIBRARIES}
${LWS_LIBRARIES}
nlohmann_json::nlohmann_json
${SYSTEMD_LIBRARIES}
)
# Installation rules
install(TARGETS yourpart-daemon DESTINATION /usr/local/bin)
# Installiere Template als Referenz ZUERST (wird vom install-Skript benötigt)
install(FILES daemon.conf DESTINATION /etc/yourpart/ RENAME daemon.conf.example)
# Intelligente Konfigurationsdatei-Installation
# Verwendet ein CMake-Skript, das nur fehlende Keys hinzufügt, ohne bestehende zu überschreiben
# Das Skript liest das Template aus /etc/yourpart/daemon.conf.example oder dem Source-Verzeichnis
install(SCRIPT cmake/install-config.cmake)

414
CMakeLists.txt.user Normal file
View File

@@ -0,0 +1,414 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 17.0.0, 2025-08-16T22:07:06. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>
<value type="QByteArray">{551ef6b3-a39b-43e2-9ee3-ad56e19ff4f4}</value>
</data>
<data>
<variable>ProjectExplorer.Project.ActiveTarget</variable>
<value type="qlonglong">0</value>
</data>
<data>
<variable>ProjectExplorer.Project.EditorSettings</variable>
<valuemap type="QVariantMap">
<value type="bool" key="EditorConfiguration.AutoDetect">true</value>
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
<value type="QString" key="language">Cpp</value>
<valuemap type="QVariantMap" key="value">
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
</valuemap>
</valuemap>
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
<value type="QString" key="language">QmlJS</value>
<valuemap type="QVariantMap" key="value">
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
</valuemap>
</valuemap>
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
<value type="int" key="EditorConfiguration.IndentSize">4</value>
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
<value type="int" key="EditorConfiguration.LineEndingBehavior">0</value>
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">2</value>
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
<value type="int" key="EditorConfiguration.TabSize">8</value>
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.PluginSettings</variable>
<valuemap type="QVariantMap">
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
<value type="bool" key="AutoTest.Framework.Boost">true</value>
<value type="bool" key="AutoTest.Framework.CTest">false</value>
<value type="bool" key="AutoTest.Framework.Catch">true</value>
<value type="bool" key="AutoTest.Framework.GTest">true</value>
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
</valuemap>
<value type="bool" key="AutoTest.ApplyFilter">false</value>
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
<valuelist type="QVariantList" key="AutoTest.PathFilters"/>
<value type="int" key="AutoTest.RunAfterBuild">0</value>
<value type="bool" key="AutoTest.UseGlobal">true</value>
<valuemap type="QVariantMap" key="ClangTools">
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
<value type="int" key="ClangTools.ParallelJobs">8</value>
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
</valuemap>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.Target.0</variable>
<valuemap type="QVariantMap">
<value type="QString" key="DeviceType">Desktop</value>
<value type="bool" key="HasPerBcDcs">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{78ff90a3-f672-45c2-ad08-343b0923896f}</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
<value type="QString" key="CMake.Build.Type">Debug</value>
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
-DCMAKE_GENERATOR:STRING=Unix Makefiles
-DCMAKE_BUILD_TYPE:STRING=Release
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build/</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">all</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">clean</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Release</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString"></value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.1">
<value type="QString" key="CMake.Build.Type">Debug</value>
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
-DCMAKE_GENERATOR:STRING=Unix Makefiles
-DCMAKE_BUILD_TYPE:STRING=Debug
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
<value type="QString" key="CMake.Source.Directory">/mnt/share/torsten/Programs/yourpart-daemon</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">all</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">clean</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Debug (importiert)</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">-1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">install</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">0</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString"></value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.TargetCount</variable>
<value type="qlonglong">1</value>
</data>
<data>
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
<value type="int">22</value>
</data>
<data>
<variable>Version</variable>
<value type="int">22</value>
</data>
</qtcreator>

205
CMakeLists.txt.user.d36652f Normal file
View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 12.0.2, 2025-07-18T07:45:58. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>
<value type="QByteArray">{d36652ff-969b-426b-a63f-1edd325096c5}</value>
</data>
<data>
<variable>ProjectExplorer.Project.ActiveTarget</variable>
<value type="qlonglong">0</value>
</data>
<data>
<variable>ProjectExplorer.Project.EditorSettings</variable>
<valuemap type="QVariantMap">
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
<value type="bool" key="EditorConfiguration.AutoSpacesForTabs">false</value>
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
<value type="QString" key="language">Cpp</value>
<valuemap type="QVariantMap" key="value">
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
</valuemap>
</valuemap>
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
<value type="QString" key="language">QmlJS</value>
<valuemap type="QVariantMap" key="value">
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
</valuemap>
</valuemap>
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
<value type="int" key="EditorConfiguration.IndentSize">4</value>
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">0</value>
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
<value type="int" key="EditorConfiguration.TabSize">8</value>
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.PluginSettings</variable>
<valuemap type="QVariantMap">
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
<value type="bool" key="AutoTest.Framework.Boost">true</value>
<value type="bool" key="AutoTest.Framework.CTest">false</value>
<value type="bool" key="AutoTest.Framework.Catch">true</value>
<value type="bool" key="AutoTest.Framework.GTest">true</value>
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
</valuemap>
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
<value type="int" key="AutoTest.RunAfterBuild">0</value>
<value type="bool" key="AutoTest.UseGlobal">true</value>
<valuemap type="QVariantMap" key="ClangTools">
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
<value type="int" key="ClangTools.ParallelJobs">8</value>
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
</valuemap>
<valuemap type="QVariantMap" key="CppEditor.QuickFix">
<value type="bool" key="UseGlobalSettings">true</value>
</valuemap>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.Target.0</variable>
<valuemap type="QVariantMap">
<value type="QString" key="DeviceType">Desktop</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{3c6cfc13-714d-4db1-bd45-b9794643cc67}</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
<value type="QString" key="CMake.Build.Type">Debug</value>
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_GENERATOR:STRING=Unix Makefiles
-DCMAKE_BUILD_TYPE:STRING=Build
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}</value>
<value type="QString" key="CMake.Source.Directory">/home/torsten/Programs/yourpart-daemon</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">all</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">clean</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<value type="QString" key="Analyzer.Valgrind.ValgrindExecutable">/usr/bin/valgrind</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.yourpart-daemon</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.TargetCount</variable>
<value type="qlonglong">1</value>
</data>
<data>
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
<value type="int">22</value>
</data>
<data>
<variable>Version</variable>
<value type="int">22</value>
</data>
</qtcreator>

230
PERFORMANCE_ANALYSIS.md Normal file
View File

@@ -0,0 +1,230 @@
# Backend Performance-Analyse: Sell-Funktionen
## Identifizierte Performance-Probleme
### 1. **N+1 Query Problem in `sellAllProducts()`**
**Problem:**
Die Funktion `sellAllProducts()` macht für jedes Inventory-Item mehrere separate Datenbankabfragen:
1. **Erste Schleife (Zeile 1702-1711):**
- `calcRegionalSellPrice()` → macht `TownProductWorth.findOne()` für jedes Item
- `getCumulativeTaxPercentWithExemptions()` → macht mehrere Queries pro Item:
- `FalukantCharacter.findOne()`
- `PoliticalOffice.findAll()` mit Includes
- Rekursive SQL-Query für Steuerberechnung
- `addSellItem()` → macht `Branch.findOne()` und `DaySell.findOne()`/`create()` für jedes Item
2. **Zweite Schleife (Zeile 1714-1724):**
- `RegionData.findOne()` für jedes Item
- `getCumulativeTaxPercent()` → rekursive SQL-Query für jedes Item
- `calcRegionalSellPrice()` → erneut `TownProductWorth.findOne()` für jedes Item
**Beispiel:** Bei 10 Items werden gemacht:
- 10x `TownProductWorth.findOne()` (2x pro Item)
- 10x `RegionData.findOne()`
- 10x `getCumulativeTaxPercentWithExemptions()` (mit mehreren Queries)
- 10x `getCumulativeTaxPercent()` (rekursive SQL)
- 10x `addSellItem()` (mit 2 Queries pro Item)
- = **~70+ Datenbankabfragen für 10 Items**
### 2. **Ineffiziente `addSellItem()` Implementierung**
**Problem:**
- Wird für jedes Item einzeln aufgerufen
- Macht `Branch.findOne()` für jedes Item (könnte gecacht werden)
- `DaySell.findOne()` und `create()`/`update()` für jedes Item
**Lösung:** Batch-Operation implementieren, die alle DaySell Einträge auf einmal verarbeitet.
### 3. **Doppelte Berechnungen in `sellAllProducts()`**
**Problem:**
- Preis wird zweimal berechnet (Zeile 1705 und 1718)
- Steuer wird zweimal berechnet (Zeile 1706 und 1717)
- `calcRegionalSellPrice()` wird zweimal aufgerufen mit denselben Parametern
### 4. **Fehlende Indizes**
**Potenzielle fehlende Indizes:**
- `falukant_data.town_product_worth(product_id, region_id)` - sollte unique sein
- `falukant_data.inventory(stock_id, product_id, quality)` - für schnelle Lookups
- `falukant_data.knowledge(character_id, product_id)` - für Knowledge-Lookups
- `falukant_data.political_office(character_id)` - für Steuerbefreiungen
### 5. **Ineffiziente `getCumulativeTaxPercentWithExemptions()`**
**Problem:**
- Lädt alle PoliticalOffices jedes Mal neu, auch wenn sich nichts geändert hat
- Macht komplexe rekursive SQL-Query für jedes Item separat
- Könnte gecacht werden (z.B. pro User+Region Kombination)
## Empfohlene Optimierungen
### 1. **Batch-Loading für `sellAllProducts()`**
```javascript
async sellAllProducts(hashedUserId, branchId) {
// ... existing code ...
// Batch-Load alle benötigten Daten VOR den Schleifen
const regionIds = [...new Set(inventory.map(item => item.stock.branch.regionId))];
const productIds = [...new Set(inventory.map(item => item.productType.id))];
// 1. Lade alle TownProductWorth Einträge auf einmal
const townWorths = await TownProductWorth.findAll({
where: {
productId: { [Op.in]: productIds },
regionId: { [Op.in]: regionIds }
}
});
const worthMap = new Map();
townWorths.forEach(tw => {
worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent);
});
// 2. Lade alle RegionData auf einmal
const regions = await RegionData.findAll({
where: { id: { [Op.in]: regionIds } }
});
const regionMap = new Map(regions.map(r => [r.id, r]));
// 3. Berechne Steuern für alle Regionen auf einmal
const taxMap = new Map();
for (const regionId of regionIds) {
const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId);
taxMap.set(regionId, tax);
}
// 4. Berechne Preise und Steuern in einer Schleife
const sellItems = [];
for (const item of inventory) {
const regionId = item.stock.branch.regionId;
const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50;
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
const pricePerUnit = calcRegionalSellPrice(item.productType, knowledgeVal, regionId, worthPercent);
const cumulativeTax = taxMap.get(regionId);
// ... rest of calculation ...
sellItems.push({ branchId: item.stock.branch.id, productId: item.productType.id, quantity: item.quantity });
}
// 5. Batch-Update DaySell Einträge
await this.addSellItemsBatch(sellItems);
// ... rest of code ...
}
```
### 2. **Batch-Operation für `addSellItem()`**
```javascript
async addSellItemsBatch(sellItems) {
// Gruppiere nach (regionId, productId, sellerId)
const grouped = new Map();
for (const item of sellItems) {
const branch = await Branch.findByPk(item.branchId);
if (!branch) continue;
const key = `${branch.regionId}-${item.productId}-${item.sellerId}`;
if (!grouped.has(key)) {
grouped.set(key, {
regionId: branch.regionId,
productId: item.productId,
sellerId: item.sellerId,
quantity: 0
});
}
grouped.get(key).quantity += item.quantity;
}
// Batch-Update oder Create
for (const [key, data] of grouped) {
const [daySell, created] = await DaySell.findOrCreate({
where: {
regionId: data.regionId,
productId: data.productId,
sellerId: data.sellerId
},
defaults: { quantity: data.quantity }
});
if (!created) {
daySell.quantity += data.quantity;
await daySell.save();
}
}
}
```
### 3. **Caching für `getCumulativeTaxPercentWithExemptions()`**
```javascript
// Cache für Steuerberechnungen (z.B. 5 Minuten)
const taxCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
const cacheKey = `${userId}-${regionId}`;
const cached = taxCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.value;
}
// ... existing calculation ...
taxCache.set(cacheKey, { value: tax, timestamp: Date.now() });
return tax;
}
```
### 4. **Optimierte `calcRegionalSellPrice()`**
```javascript
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
// Wenn worthPercent nicht übergeben wurde UND wir es nicht aus dem Cache haben,
// dann hole es aus der DB
if (worthPercent === null) {
const townWorth = await TownProductWorth.findOne({
where: { productId: product.id, regionId: regionId }
});
worthPercent = townWorth?.worthPercent || 50;
}
// ... rest of calculation ...
}
```
### 5. **Datenbank-Indizes hinzufügen**
```sql
-- Index für town_product_worth (sollte unique sein)
CREATE UNIQUE INDEX IF NOT EXISTS idx_town_product_worth_product_region
ON falukant_data.town_product_worth(product_id, region_id);
-- Index für inventory Lookups
CREATE INDEX IF NOT EXISTS idx_inventory_stock_product_quality
ON falukant_data.inventory(stock_id, product_id, quality);
-- Index für knowledge Lookups
CREATE INDEX IF NOT EXISTS idx_knowledge_character_product
ON falukant_data.knowledge(character_id, product_id);
-- Index für political_office Lookups
CREATE INDEX IF NOT EXISTS idx_political_office_character
ON falukant_data.political_office(character_id);
```
## Geschätzter Performance-Gewinn
- **Vorher:** ~70+ Queries für 10 Items
- **Nachher:** ~15-20 Queries für 10 Items (Batch-Loading + Caching)
- **Geschätzte Verbesserung:** 70-80% weniger Datenbankabfragen
## Priorität
1. **Hoch:** Batch-Loading für `sellAllProducts()` (größter Impact)
2. **Hoch:** Batch-Operation für `addSellItem()`
3. **Mittel:** Caching für Steuerberechnungen
4. **Mittel:** Datenbank-Indizes
5. **Niedrig:** Doppelte Berechnungen entfernen

601
SELL_OVERVIEW.md Normal file
View File

@@ -0,0 +1,601 @@
# Übersicht: Sell-Funktionen und verwendete Models/Tabellen
## Sell-Funktionen in `falukantService.js`
### 1. `sellProduct(hashedUserId, branchId, productId, quality, quantity)`
Verkauft ein einzelnes Produkt mit bestimmter Qualität.
**Ablauf:**
1. Lädt User, Branch, Character, Stock
2. Lädt Inventory mit ProductType und Knowledge
3. Berechnet Preis pro Einheit mit `calcRegionalSellPrice()`
4. Berechnet kumulative Steuer mit politischen Befreiungen
5. Passt Preis an (Inflation basierend auf Steuer)
6. Berechnet Revenue, Tax, Net
7. Aktualisiert Geld für Verkäufer und Treasury
8. Entfernt verkaufte Items aus Inventory
9. Erstellt/aktualisiert DaySell Eintrag
10. Sendet Socket-Notifications
**Verwendete Models/Tabellen:**
- `FalukantUser` (`falukant_data.falukant_user`)
- `Branch` (`falukant_data.branch`)
- `FalukantCharacter` (`falukant_data.character`)
- `FalukantStock` (`falukant_data.stock`)
- `Inventory` (`falukant_data.inventory`)
- `ProductType` (`falukant_type.product`)
- `Knowledge` (`falukant_data.knowledge`)
- `TownProductWorth` (`falukant_data.town_product_worth`)
- `RegionData` (`falukant_data.region`)
- `RegionType` (`falukant_type.region`)
- `PoliticalOffice` (`falukant_data.political_office`)
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
- `DaySell` (`falukant_log.day_sell`)
- `MoneyFlow` (via `updateFalukantUserMoney`)
### 2. `sellAllProducts(hashedUserId, branchId)`
Verkauft alle Produkte eines Branches.
**Ablauf:**
1. Lädt User, Branch mit Stocks
2. Lädt alle Inventory Items mit ProductType, Knowledge, Stock, Branch
3. Für jedes Item:
- Berechnet Preis pro Einheit
- Berechnet kumulative Steuer
- Passt Preis an
- Erstellt/aktualisiert DaySell Eintrag
4. Berechnet Gesamt-Tax pro Region
5. Aktualisiert Geld für Verkäufer und Treasury
6. Löscht alle Inventory Items
7. Sendet Socket-Notifications
**Verwendete Models/Tabellen:**
- `FalukantUser` (`falukant_data.falukant_user`)
- `Branch` (`falukant_data.branch`)
- `FalukantStock` (`falukant_data.stock`)
- `FalukantStockType` (`falukant_type.stock`)
- `FalukantCharacter` (`falukant_data.character`)
- `Inventory` (`falukant_data.inventory`)
- `ProductType` (`falukant_type.product`)
- `Knowledge` (`falukant_data.knowledge`)
- `TownProductWorth` (`falukant_data.town_product_worth`)
- `RegionData` (`falukant_data.region`)
- `RegionType` (`falukant_type.region`)
- `PoliticalOffice` (`falukant_data.political_office`)
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
- `DaySell` (`falukant_log.day_sell`)
- `MoneyFlow` (via `updateFalukantUserMoney`)
### 3. `addSellItem(branchId, userId, productId, quantity)`
Erstellt oder aktualisiert einen DaySell Eintrag für einen Verkauf.
**Ablauf:**
1. Lädt Branch
2. Sucht nach existierendem DaySell Eintrag
3. Erstellt neuen oder aktualisiert existierenden Eintrag
**Verwendete Models/Tabellen:**
- `Branch` (`falukant_data.branch`)
- `DaySell` (`falukant_log.day_sell`)
## Hilfsfunktionen
### `calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null)`
Berechnet den Verkaufspreis eines Produkts basierend auf:
- Basispreis (`product.sellCost`)
- Regionalem Worth-Percent (`town_product_worth.worth_percent`)
- Knowledge-Faktor (0-100)
**Verwendete Models/Tabellen:**
- `ProductType` (`falukant_type.product`)
- `TownProductWorth` (`falukant_data.town_product_worth`)
### `getCumulativeTaxPercent(regionId)`
Berechnet die kumulative Steuer für eine Region und alle Vorfahren (rekursiv).
**SQL Query:**
```sql
WITH RECURSIVE ancestors AS (
SELECT id, parent_id, tax_percent
FROM falukant_data.region r
WHERE id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent
FROM falukant_data.region reg
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;
```
**Verwendete Tabellen:**
- `falukant_data.region`
### `getCumulativeTaxPercentWithExemptions(userId, regionId)`
Berechnet die kumulative Steuer mit politischen Befreiungen.
**Ablauf:**
1. Lädt Character des Users
2. Lädt alle PoliticalOffices des Characters
3. Bestimmt befreite Region-Typen basierend auf Ämtern
4. Berechnet kumulative Steuer, aber schließt befreite Region-Typen aus
**SQL Query:**
```sql
WITH RECURSIVE ancestors AS (
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
FROM falukant_data.region r
JOIN falukant_type.region rt ON rt.id = r.region_type_id
WHERE r.id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
FROM falukant_data.region reg
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(CASE WHEN ARRAY[...] && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;
```
**Verwendete Models/Tabellen:**
- `FalukantCharacter` (`falukant_data.character`)
- `PoliticalOffice` (`falukant_data.political_office`)
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
- `RegionData` (`falukant_data.region`)
- `RegionType` (`falukant_type.region`)
**Politische Steuerbefreiungen:**
```javascript
const POLITICAL_TAX_EXEMPTIONS = {
'council': ['city'],
'taxman': ['city', 'county'],
'treasurerer': ['city', 'county', 'shire'],
'super-state-administrator': ['city', 'county', 'shire', 'markgrave', 'duchy'],
'chancellor': ['city','county','shire','markgrave','duchy'] // = alle Typen
};
```
## Model-Definitionen
### Inventory (`falukant_data.inventory`)
```javascript
// backend/models/falukant/data/inventory.js
- id
- stockId (FK zu falukant_data.stock)
- productId (FK zu falukant_type.product)
- quantity
- quality
- producedAt
```
### DaySell (`falukant_log.day_sell`)
```javascript
// backend/models/falukant/log/daysell.js
- id
- regionId (FK zu falukant_data.region)
- productId (FK zu falukant_type.product)
- sellerId (FK zu falukant_data.falukant_user)
- quantity
- createdAt
- updatedAt
```
### TownProductWorth (`falukant_data.town_product_worth`)
```javascript
// backend/models/falukant/data/town_product_worth.js
- id
- productId (FK zu falukant_type.product)
- regionId (FK zu falukant_data.region)
- worthPercent (0-100)
```
### Knowledge (`falukant_data.knowledge`)
```javascript
// backend/models/falukant/data/product_knowledge.js
- id
- productId (FK zu falukant_type.product)
- characterId (FK zu falukant_data.character)
- knowledge (0-99)
```
## Wichtige SQL-Queries
### 1. Inventory mit ProductType und Knowledge laden
```javascript
Inventory.findAll({
where: { quality },
include: [
{
model: ProductType,
as: 'productType',
required: true,
where: { id: productId },
include: [
{
model: Knowledge,
as: 'knowledges',
required: false,
where: { characterId: character.id }
}
]
}
]
})
```
### 2. Kumulative Steuer mit Befreiungen berechnen
Siehe `getCumulativeTaxPercentWithExemptions()` oben.
## Preisberechnung
### Formel für `calcRegionalSellPrice`:
1. Basispreis = `product.sellCost * (worthPercent / 100)`
2. Min = `basePrice * 0.6`
3. Max = `basePrice`
4. Preis = `min + (max - min) * (knowledgeFactor / 100)`
### Steueranpassung:
1. `inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100))`
2. `adjustedPricePerUnit = pricePerUnit * inflationFactor`
3. `revenue = quantity * adjustedPricePerUnit`
4. `taxValue = revenue * cumulativeTax / 100`
5. `net = revenue - taxValue`
## Vollständige Code-Snippets
### `calcRegionalSellPrice()`
```javascript
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
if (worthPercent === null) {
const townWorth = await TownProductWorth.findOne({
where: { productId: product.id, regionId: regionId }
});
worthPercent = townWorth?.worthPercent || 50; // Default 50% wenn nicht gefunden
}
// Basispreis basierend auf regionalem worthPercent
const basePrice = product.sellCost * (worthPercent / 100);
// Dann Knowledge-Faktor anwenden
const min = basePrice * 0.6;
const max = basePrice;
return min + (max - min) * (knowledgeFactor / 100);
}
```
### `getCumulativeTaxPercent()`
```javascript
async function getCumulativeTaxPercent(regionId) {
if (!regionId) return 0;
const rows = await sequelize.query(
`WITH RECURSIVE ancestors AS (
SELECT id, parent_id, tax_percent
FROM falukant_data.region r
WHERE id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent
FROM falukant_data.region reg
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;`,
{
replacements: { id: regionId },
type: sequelize.QueryTypes.SELECT
}
);
const val = rows?.[0]?.total ?? 0;
return parseFloat(val) || 0;
}
```
### `getCumulativeTaxPercentWithExemptions()` (vereinfacht)
```javascript
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
if (!regionId) return 0;
// Character finden
const character = await FalukantCharacter.findOne({
where: { userId },
attributes: ['id']
});
if (!character) return 0;
// Politische Ämter laden
const offices = await PoliticalOffice.findAll({
where: { characterId: character.id },
include: [
{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] },
{
model: RegionData,
as: 'region',
include: [{
model: RegionType,
as: 'regionType',
attributes: ['labelTr']
}]
}
]
});
// Befreite Region-Typen bestimmen
const exemptTypes = new Set();
let hasChancellor = false;
for (const o of offices) {
const name = o.type?.name;
if (!name) continue;
if (name === 'chancellor') { hasChancellor = true; break; }
const allowed = POLITICAL_TAX_EXEMPTIONS[name];
if (allowed && Array.isArray(allowed)) {
for (const t of allowed) exemptTypes.add(t);
}
}
if (hasChancellor) return 0;
// SQL Query mit Befreiungen
const exemptTypesArray = Array.from(exemptTypes);
const exemptTypesString = exemptTypesArray.length > 0
? `ARRAY[${exemptTypesArray.map(t => `'${t.replace(/'/g, "''")}'`).join(',')}]`
: `ARRAY[]::text[]`;
const rows = await sequelize.query(
`WITH RECURSIVE ancestors AS (
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
FROM falukant_data.region r
JOIN falukant_type.region rt ON rt.id = r.region_type_id
WHERE r.id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
FROM falukant_data.region reg
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(CASE WHEN ${exemptTypesString} && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;`,
{
replacements: { id: regionId },
type: sequelize.QueryTypes.SELECT
}
);
const val = rows?.[0]?.total ?? 0;
return parseFloat(val) || 0;
}
```
### `sellProduct()` (Kern-Logik)
```javascript
async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
const user = await getFalukantUserOrFail(hashedUserId);
const branch = await getBranchOrFail(user.id, branchId);
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) throw new Error('No character found for user');
const stock = await FalukantStock.findOne({ where: { branchId: branch.id } });
if (!stock) throw new Error('Stock not found');
// Inventory laden
const inventory = await Inventory.findAll({
where: { quality },
include: [
{
model: ProductType,
as: 'productType',
required: true,
where: { id: productId },
include: [
{
model: Knowledge,
as: 'knowledges',
required: false,
where: { characterId: character.id }
}
]
}
]
});
if (!inventory.length) throw new Error('No inventory found');
const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
if (available < quantity) throw new Error('Not enough inventory available');
const item = inventory[0].productType;
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
// Steuer berechnen
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
const revenue = quantity * adjustedPricePerUnit;
// Tax und Net berechnen
const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
const net = Math.round((revenue - taxValue) * 100) / 100;
// Geld aktualisieren
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id);
if (!moneyResult.success) throw new Error('Failed to update money for seller');
// Steuer an Treasury buchen
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
if (treasuryId && taxValue > 0) {
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id);
if (!taxResult.success) throw new Error('Failed to update money for treasury');
}
// Inventory aktualisieren
let remaining = quantity;
for (const inv of inventory) {
if (inv.quantity <= remaining) {
remaining -= inv.quantity;
await inv.destroy();
} else {
await inv.update({ quantity: inv.quantity - remaining });
remaining = 0;
break;
}
}
// DaySell Eintrag erstellen/aktualisieren
await this.addSellItem(branchId, user.id, productId, quantity);
// Notifications senden
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
return { success: true };
}
```
### `addSellItem()`
```javascript
async addSellItem(branchId, userId, productId, quantity) {
const branch = await Branch.findOne({
where: { id: branchId },
});
const daySell = await DaySell.findOne({
where: {
regionId: branch.regionId,
productId: productId,
sellerId: userId,
}
});
if (daySell) {
daySell.quantity += quantity;
await daySell.save();
} else {
await DaySell.create({
regionId: branch.regionId,
productId: productId,
sellerId: userId,
quantity: quantity,
});
}
}
```
## Wichtige Hinweise
1. **Inventory wird nach Verkauf gelöscht/aktualisiert**: Items werden aus der Inventory entfernt oder die Menge reduziert.
2. **DaySell wird aggregiert**: Wenn bereits ein DaySell Eintrag für Region/Product/Seller existiert, wird die Menge addiert.
3. **Steuer wird an Treasury gebucht**: Wenn `TREASURY_FALUKANT_USER_ID` gesetzt ist, wird die Steuer an diesen User gebucht.
4. **Socket-Notifications**: Nach jedem Verkauf werden `falukantUpdateStatus` und `falukantBranchUpdate` Events gesendet.
5. **Politische Befreiungen**: Bestimmte politische Ämter befreien von Steuern in bestimmten Region-Typen. Chancellor befreit von allen Steuern.
6. **Preis-Inflation**: Der Preis wird basierend auf der Steuer inflatiert, damit der Netto-Betrag für den Verkäufer gleich bleibt.
## Tabellenübersicht
### `falukant_data.inventory`
- `id` (PK)
- `stock_id` (FK zu `falukant_data.stock`)
- `product_id` (FK zu `falukant_type.product`)
- `quantity` (INTEGER)
- `quality` (INTEGER)
- `produced_at` (DATE)
### `falukant_log.sell` (DaySell)
- `id` (PK)
- `region_id` (FK zu `falukant_data.region`)
- `product_id` (FK zu `falukant_type.product`)
- `seller_id` (FK zu `falukant_data.falukant_user`)
- `quantity` (INTEGER)
- `sell_timestamp` (DATE)
- **Unique Index**: `(seller_id, product_id, region_id)`
### `falukant_data.town_product_worth`
- `id` (PK)
- `product_id` (FK zu `falukant_type.product`)
- `region_id` (FK zu `falukant_data.region`)
- `worth_percent` (INTEGER, 0-100)
### `falukant_data.knowledge`
- `id` (PK)
- `product_id` (FK zu `falukant_type.product`)
- `character_id` (FK zu `falukant_data.character`)
- `knowledge` (INTEGER, 0-99)
### `falukant_data.political_office`
- `id` (PK)
- `office_type_id` (FK zu `falukant_type.political_office_type`)
- `character_id` (FK zu `falukant_data.character`)
- `region_id` (FK zu `falukant_data.region`)
- `created_at`, `updated_at`
### `falukant_type.political_office_type`
- `id` (PK)
- `name` (STRING) - z.B. 'council', 'taxman', 'treasurerer', 'super-state-administrator', 'chancellor'
- `seats_per_region` (INTEGER)
- `region_type` (STRING)
- `term_length` (INTEGER)
### `falukant_data.region`
- `id` (PK)
- `name` (STRING)
- `region_type_id` (FK zu `falukant_type.region`)
- `parent_id` (FK zu `falukant_data.region`, nullable)
- `map` (JSONB)
- `tax_percent` (DECIMAL)
### `falukant_type.region`
- `id` (PK)
- `label_tr` (STRING) - z.B. 'city', 'county', 'shire', 'markgrave', 'duchy'
- `parent_id` (FK zu `falukant_type.region`, nullable)
### `falukant_data.falukant_user`
- `id` (PK)
- `user_id` (FK zu `community.user`)
- `money` (DECIMAL)
- `credit_amount`, `today_credit_taken`, `credit_interest_rate`
- `certificate`
- `main_branch_region_id`
- `last_nobility_advance_at`
- `created_at`, `updated_at`
### `falukant_data.character`
- `id` (PK)
- `user_id` (FK zu `falukant_data.falukant_user`)
- `region_id` (FK zu `falukant_data.region`)
- `first_name`, `last_name`
- `birthdate`, `gender`, `health`
- `title_of_nobility` (FK zu `falukant_type.title_of_nobility`)
- `mood_id` (FK zu `falukant_type.mood`)
- `created_at`, `updated_at`
### `falukant_data.branch`
- `id` (PK)
- `branch_type_id` (FK zu `falukant_type.branch`)
- `region_id` (FK zu `falukant_data.region`)
- `falukant_user_id` (FK zu `falukant_data.falukant_user`)
### `falukant_data.stock`
- `id` (PK)
- `branch_id` (FK zu `falukant_data.branch`)
- `stock_type_id` (FK zu `falukant_type.stock`)
- `quantity` (INTEGER)
- `product_quality` (INTEGER, nullable)
### `falukant_type.product`
- `id` (PK)
- `label_tr` (STRING, unique)
- `category` (INTEGER)
- `production_time` (INTEGER)
- `sell_cost` (INTEGER)
## Dateipfade
- **Service**: `backend/services/falukantService.js`
- **Models**:
- `backend/models/falukant/data/inventory.js`
- `backend/models/falukant/log/daysell.js`
- `backend/models/falukant/data/town_product_worth.js`
- `backend/models/falukant/data/product_knowledge.js`
- `backend/models/falukant/data/political_office.js`
- `backend/models/falukant/type/political_office_type.js`
- `backend/models/falukant/data/region.js`
- `backend/models/falukant/type/region.js`
- `backend/models/falukant/data/character.js`
- `backend/models/falukant/data/user.js`
- `backend/models/falukant/data/branch.js`
- `backend/models/falukant/data/stock.js`
- `backend/models/falukant/type/product.js`

168
SSL-SETUP.md Normal file
View File

@@ -0,0 +1,168 @@
# SSL/TLS Setup für YourPart Daemon
Dieses Dokument beschreibt, wie Sie SSL/TLS-Zertifikate für den YourPart Daemon einrichten können.
## 🚀 Schnellstart
### 1. Self-Signed Certificate (Entwicklung/Testing)
```bash
./setup-ssl.sh
# Wählen Sie Option 1
```
### 2. Let's Encrypt Certificate (Produktion)
```bash
./setup-ssl.sh
# Wählen Sie Option 2
```
### 3. Apache2-Zertifikate verwenden (empfohlen für Ubuntu)
```bash
./setup-ssl.sh
# Wählen Sie Option 4
# Verwendet bereits vorhandene Apache2-Zertifikate
# ⚠️ Warnung bei Snakeoil-Zertifikaten (nur für localhost)
```
### 4. DNS-01 Challenge (für komplexe Setups)
```bash
./setup-ssl-dns.sh
# Für Cloudflare, Route53, etc.
```
## 📋 Voraussetzungen
### Für Apache2-Zertifikate:
- Apache2 installiert oder Zertifikate in Standard-Pfaden
- Unterstützte Pfade (priorisiert nach Qualität):
- `/etc/letsencrypt/live/your-part.de/fullchain.pem` (Let's Encrypt - empfohlen)
- `/etc/letsencrypt/live/$(hostname)/fullchain.pem` (Let's Encrypt)
- `/etc/apache2/ssl/apache.crt` (Custom Apache2)
- `/etc/ssl/certs/ssl-cert-snakeoil.pem` (Ubuntu Standard - nur localhost)
### Für Let's Encrypt (HTTP-01 Challenge):
- Port 80 muss verfügbar sein
- Domain `your-part.de` muss auf den Server zeigen
- Kein anderer Service auf Port 80
### Für DNS-01 Challenge:
- DNS-Provider Account (Cloudflare, Route53, etc.)
- API-Credentials für DNS-Management
## 🔧 Konfiguration
Nach der Zertifikats-Erstellung:
1. **SSL in der Konfiguration aktivieren:**
```ini
# /etc/yourpart/daemon.conf
WEBSOCKET_SSL_ENABLED=true
WEBSOCKET_SSL_CERT_PATH=/etc/yourpart/server.crt
WEBSOCKET_SSL_KEY_PATH=/etc/yourpart/server.key
```
2. **Daemon neu starten:**
```bash
sudo systemctl restart yourpart-daemon
```
3. **Verbindung testen:**
```bash
# WebSocket Secure
wss://your-part.de:4551
# Oder ohne SSL
ws://your-part.de:4551
```
## 🔄 Automatische Erneuerung
### Let's Encrypt-Zertifikate:
- **Cron Job:** Täglich um 2:30 Uhr
- **Script:** `/etc/yourpart/renew-ssl.sh`
- **Log:** `/var/log/yourpart/ssl-renewal.log`
### Apache2-Zertifikate:
- **Ubuntu Snakeoil:** Automatisch von Apache2 verwaltet
- **Let's Encrypt:** Automatische Erneuerung wenn erkannt
- **Custom:** Manuelle Verwaltung erforderlich
## 📁 Dateistruktur
```
/etc/yourpart/
├── server.crt # Zertifikat (Symlink zu Let's Encrypt)
├── server.key # Private Key (Symlink zu Let's Encrypt)
├── renew-ssl.sh # Auto-Renewal Script
└── cloudflare.ini # Cloudflare Credentials (falls verwendet)
/etc/letsencrypt/live/your-part.de/
├── fullchain.pem # Vollständige Zertifikatskette
├── privkey.pem # Private Key
├── cert.pem # Zertifikat
└── chain.pem # Intermediate Certificate
```
## 🛠️ Troubleshooting
### Zertifikat wird nicht akzeptiert
```bash
# Prüfe Zertifikats-Gültigkeit
openssl x509 -in /etc/yourpart/server.crt -text -noout
# Prüfe Berechtigungen
ls -la /etc/yourpart/server.*
```
### Let's Encrypt Challenge fehlgeschlagen
```bash
# Prüfe Port 80
sudo netstat -tlnp | grep :80
# Prüfe DNS
nslookup your-part.de
# Prüfe Firewall
sudo ufw status
```
### Auto-Renewal funktioniert nicht
```bash
# Prüfe Cron Jobs
sudo crontab -l
# Teste Renewal Script
sudo /etc/yourpart/renew-ssl.sh
# Prüfe Logs
tail -f /var/log/yourpart/ssl-renewal.log
```
## 🔒 Sicherheit
### Berechtigungen
- **Zertifikat:** `644` (readable by all, writable by owner)
- **Private Key:** `600` (readable/writable by owner only)
- **Owner:** `yourpart:yourpart`
### Firewall
```bash
# Öffne Port 80 für Let's Encrypt Challenge
sudo ufw allow 80/tcp
# Öffne Port 4551 für WebSocket
sudo ufw allow 4551/tcp
```
## 📚 Weitere Informationen
- [Let's Encrypt Dokumentation](https://letsencrypt.org/docs/)
- [Certbot Dokumentation](https://certbot.eff.org/docs/)
- [libwebsockets SSL](https://libwebsockets.org/lws-api-doc-master/html/group__ssl.html)
## 🆘 Support
Bei Problemen:
1. Prüfen Sie die Logs: `sudo journalctl -u yourpart-daemon -f`
2. Testen Sie die Zertifikate: `openssl s_client -connect your-part.de:4551`
3. Prüfen Sie die Firewall: `sudo ufw status`

23
backend/README_TAX.md Normal file
View 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.

View File

@@ -1,6 +1,7 @@
import express from 'express'; import express from 'express';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import crypto from 'crypto';
import chatRouter from './routers/chatRouter.js'; import chatRouter from './routers/chatRouter.js';
import authRouter from './routers/authRouter.js'; import authRouter from './routers/authRouter.js';
import navigationRouter from './routers/navigationRouter.js'; import navigationRouter from './routers/navigationRouter.js';
@@ -16,6 +17,8 @@ import match3Router from './routers/match3Router.js';
import taxiRouter from './routers/taxiRouter.js'; import taxiRouter from './routers/taxiRouter.js';
import taxiMapRouter from './routers/taxiMapRouter.js'; import taxiMapRouter from './routers/taxiMapRouter.js';
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js'; import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
import termineRouter from './routers/termineRouter.js';
import vocabRouter from './routers/vocabRouter.js';
import cors from 'cors'; import cors from 'cors';
import './jobs/sessionCleanup.js'; import './jobs/sessionCleanup.js';
@@ -24,6 +27,25 @@ const __dirname = path.dirname(__filename);
const app = express(); const app = express();
// Request-Timing (aktivierbar per ENV)
// - LOG_SLOW_REQ_MS=200: Logge Requests, die länger dauern als X ms (Default 500)
// - LOG_ALL_REQ=1: Logge alle Requests
const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1';
const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10);
app.use((req, res, next) => {
const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex'));
req.reqId = reqId;
res.setHeader('x-request-id', reqId);
const t0 = Date.now();
res.on('finish', () => {
const ms = Date.now() - t0;
if (LOG_ALL_REQ || ms >= LOG_SLOW_REQ_MS) {
console.log(`⏱️ REQ ${ms}ms ${res.statusCode} ${req.method} ${req.originalUrl} rid=${reqId}`);
}
});
next();
});
const corsOptions = { const corsOptions = {
origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'], origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
@@ -48,10 +70,12 @@ app.use('/api/taxi/highscores', taxiHighscoreRouter);
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images'))); app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
app.use('/api/contact', contactRouter); app.use('/api/contact', contactRouter);
app.use('/api/socialnetwork', socialnetworkRouter); app.use('/api/socialnetwork', socialnetworkRouter);
app.use('/api/vocab', vocabRouter);
app.use('/api/forum', forumRouter); app.use('/api/forum', forumRouter);
app.use('/api/falukant', falukantRouter); app.use('/api/falukant', falukantRouter);
app.use('/api/friendships', friendshipRouter); app.use('/api/friendships', friendshipRouter);
app.use('/api/blog', blogRouter); app.use('/api/blog', blogRouter);
app.use('/api/termine', termineRouter);
// Serve frontend SPA for non-API routes to support history mode clean URLs // Serve frontend SPA for non-API routes to support history mode clean URLs
const frontendDir = path.join(__dirname, '../frontend'); const frontendDir = path.join(__dirname, '../frontend');

View File

@@ -27,6 +27,7 @@ class AdminController {
// User administration // User administration
this.searchUsers = this.searchUsers.bind(this); this.searchUsers = this.searchUsers.bind(this);
this.getUser = this.getUser.bind(this); this.getUser = this.getUser.bind(this);
this.getUsers = this.getUsers.bind(this);
this.updateUser = this.updateUser.bind(this); this.updateUser = this.updateUser.bind(this);
// Rights // Rights
@@ -37,6 +38,14 @@ class AdminController {
// Statistics // Statistics
this.getUserStatistics = this.getUserStatistics.bind(this); 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);
this.createNPCs = this.createNPCs.bind(this);
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
} }
async getOpenInterests(req, res) { async getOpenInterests(req, res) {
@@ -74,6 +83,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) { async updateUser(req, res) {
try { try {
const { userid: requester } = req.headers; const { userid: requester } = req.headers;
@@ -290,6 +323,122 @@ 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 createNPCs(req, res) {
try {
const { userid: userId } = req.headers;
const { regionIds, minAge, maxAge, minTitleId, maxTitleId, count } = req.body;
const countValue = parseInt(count) || 1;
if (countValue < 1 || countValue > 500) {
return res.status(400).json({ error: 'Count must be between 1 and 500' });
}
console.log('[createNPCs] Request received:', { userId, regionIds, minAge, maxAge, minTitleId, maxTitleId, count: countValue });
const result = await AdminService.createNPCs(userId, {
regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
minAge: parseInt(minAge) || 0,
maxAge: parseInt(maxAge) || 100,
minTitleId: parseInt(minTitleId) || 1,
maxTitleId: parseInt(maxTitleId) || 19,
count: countValue
});
console.log('[createNPCs] Job created:', result);
res.status(200).json(result);
} catch (error) {
console.error('[createNPCs] Error:', error);
console.error('[createNPCs] Error stack:', error.stack);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message || 'Internal server error' });
}
}
async getTitlesOfNobility(req, res) {
try {
const { userid: userId } = req.headers;
const titles = await AdminService.getTitlesOfNobility(userId);
res.status(200).json(titles);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async getNPCsCreationStatus(req, res) {
try {
const { userid: userId } = req.headers;
const { jobId } = req.params;
const status = await AdminService.getNPCsCreationStatus(userId, jobId);
res.status(200).json(status);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' || error.message === 'Access denied' ? 403 :
error.message === 'Job not found' ? 404 : 500;
res.status(status).json({ error: error.message });
}
}
async getRoomTypes(req, res) { async getRoomTypes(req, res) {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;

View File

@@ -30,6 +30,7 @@ class FalukantController {
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId)); 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.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch)); 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) => { this.createProduction = this._wrapWithUser((userId, req) => {
const { branchId, productId, quantity } = req.body; const { branchId, productId, quantity } = req.body;
return this.service.createProduction(userId, branchId, productId, quantity); return this.service.createProduction(userId, branchId, productId, quantity);
@@ -91,6 +92,9 @@ class FalukantController {
if (!result) throw { status: 404, message: 'No family data found' }; if (!result) throw { status: 404, message: 'No family data found' };
return result; return result;
}); });
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId)); this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.getGifts = this._wrapWithUser((userId) => { this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId); console.log('🔍 getGifts called with userId:', userId);
@@ -114,6 +118,12 @@ class FalukantController {
}, { successStatus: 201 }); }, { successStatus: 201 });
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId)); this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) => {
const { actionTypeId } = req.body;
return this.service.executeReputationAction(userId, actionTypeId);
}, { successStatus: 201 });
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId)); this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
this.baptise = this._wrapWithUser((userId, req) => { this.baptise = this._wrapWithUser((userId, req) => {
const { characterId: childId, firstName } = req.body; const { characterId: childId, firstName } = req.body;
@@ -143,6 +153,36 @@ class FalukantController {
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds)); this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId)); 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.getProductPricesInRegionBatch = this._wrapWithUser((userId, req) => {
const productIds = req.query.productIds;
const regionId = parseInt(req.query.regionId, 10);
if (!productIds || Number.isNaN(regionId)) {
throw new Error('productIds (comma-separated) and regionId are required');
}
const productIdArray = productIds.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !Number.isNaN(id));
if (productIdArray.length === 0) {
throw new Error('At least one valid productId is required');
}
return this.service.getProductPricesInRegionBatch(userId, productIdArray, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice);
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.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId)); this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
@@ -181,6 +221,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 }
);
} }

View File

@@ -4,6 +4,7 @@ import UserRight from '../models/community/user_right.js';
import UserRightType from '../models/type/user_right.js'; import UserRightType from '../models/type/user_right.js';
import UserParamType from '../models/type/user_param.js'; import UserParamType from '../models/type/user_param.js';
import FalukantUser from '../models/falukant/data/user.js'; import FalukantUser from '../models/falukant/data/user.js';
import VocabService from '../services/vocabService.js';
const menuStructure = { const menuStructure = {
home: { home: {
@@ -49,6 +50,11 @@ const menuStructure = {
visible: ["all"], visible: ["all"],
path: "/socialnetwork/gallery" path: "/socialnetwork/gallery"
}, },
vocabtrainer: {
visible: ["all"],
path: "/socialnetwork/vocab",
children: {}
},
blockedUsers: { blockedUsers: {
visible: ["all"], visible: ["all"],
path: "/socialnetwork/blocked" path: "/socialnetwork/blocked"
@@ -117,10 +123,6 @@ const menuStructure = {
visible: ["hasfalukantaccount"], visible: ["hasfalukantaccount"],
path: "/falukant/branch" path: "/falukant/branch"
}, },
directors: {
visible: ["hasfalukantaccount"],
path: "/falukant/directors"
},
family: { family: {
visible: ["hasfalukantaccount"], visible: ["hasfalukantaccount"],
path: "/falukant/family" path: "/falukant/family"
@@ -251,6 +253,10 @@ const menuStructure = {
visible: ["mainadmin", "chatrooms"], visible: ["mainadmin", "chatrooms"],
path: "/admin/chatrooms" path: "/admin/chatrooms"
}, },
servicesStatus: {
visible: ["mainadmin"],
path: "/admin/services/status"
},
interests: { interests: {
visible: ["mainadmin", "interests"], visible: ["mainadmin", "interests"],
path: "/admin/interests" path: "/admin/interests"
@@ -270,6 +276,14 @@ const menuStructure = {
visible: ["mainadmin", "falukant"], visible: ["mainadmin", "falukant"],
path: "/admin/falukant/database" path: "/admin/falukant/database"
}, },
mapEditor: {
visible: ["mainadmin", "falukant"],
path: "/admin/falukant/map"
},
createNPC: {
visible: ["mainadmin", "falukant"],
path: "/admin/falukant/create-npc"
},
} }
}, },
minigames: { minigames: {
@@ -292,6 +306,7 @@ const menuStructure = {
class NavigationController { class NavigationController {
constructor() { constructor() {
this.menu = this.menu.bind(this); this.menu = this.menu.bind(this);
this.vocabService = new VocabService();
} }
calculateAge(birthDate) { calculateAge(birthDate) {
@@ -361,6 +376,24 @@ class NavigationController {
const age = this.calculateAge(birthDate); const age = this.calculateAge(birthDate);
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean); const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id); const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
// Dynamisches Submenü: Treffpunkt → Vokabeltrainer → (Neue Sprache + abonnierte/angelegte)
// Wichtig: "Neue Sprache" soll IMMER sichtbar sein auch wenn die DB-Abfrage (noch) fehlschlägt.
if (filteredMenu?.socialnetwork?.children?.vocabtrainer) {
const children = {
newLanguage: { path: '/socialnetwork/vocab/new' },
};
try {
const langs = await this.vocabService.listLanguagesForMenu(user.id);
for (const l of langs) {
children[`lang_${l.id}`] = { path: `/socialnetwork/vocab/${l.id}`, label: l.name };
}
} catch (e) {
console.warn('[menu] Konnte Vokabeltrainer-Sprachen nicht laden:', e?.message || e);
}
filteredMenu.socialnetwork.children.vocabtrainer.children = children;
}
res.status(200).json(filteredMenu); res.status(200).json(filteredMenu);
} catch (error) { } catch (error) {
console.error('Error fetching menu:', error); console.error('Error fetching menu:', error);

View 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();

View File

@@ -0,0 +1,46 @@
import VocabService from '../services/vocabService.js';
function extractHashedUserId(req) {
return req.headers?.userid;
}
class VocabController {
constructor() {
this.service = new VocabService();
this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId));
this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 });
this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 });
this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId));
this.listChapters = this._wrapWithUser((userId, req) => this.service.listChapters(userId, req.params.languageId));
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
}
_wrapWithUser(fn, { successStatus = 200 } = {}) {
return async (req, res) => {
try {
const hashedUserId = extractHashedUserId(req);
if (!hashedUserId) {
return res.status(400).json({ error: 'Missing user identifier' });
}
const result = await fn(hashedUserId, req, res);
res.status(successStatus).json(result);
} catch (error) {
console.error('Controller error:', error);
const status = error.status && typeof error.status === 'number' ? error.status : 500;
res.status(status).json({ error: error.message || 'Internal error' });
}
};
}
}
export default VocabController;

123
backend/daemonServer.js Normal file
View File

@@ -0,0 +1,123 @@
import WebSocket, { WebSocketServer } from 'ws';
import https from 'https';
import fs from 'fs';
const PORT = Number.parseInt(process.env.DAEMON_PORT || '4551', 10);
const USE_TLS = process.env.DAEMON_TLS === '1';
const TLS_KEY_PATH = process.env.DAEMON_TLS_KEY_PATH;
const TLS_CERT_PATH = process.env.DAEMON_TLS_CERT_PATH;
const TLS_CA_PATH = process.env.DAEMON_TLS_CA_PATH; // optional
// Einfache In-Memory-Struktur für Verbindungen (für spätere Erweiterungen)
const connections = new Set();
function createServer() {
let wss;
if (USE_TLS) {
if (!TLS_KEY_PATH || !TLS_CERT_PATH) {
console.error('[Daemon] DAEMON_TLS=1 gesetzt, aber DAEMON_TLS_KEY_PATH/DAEMON_TLS_CERT_PATH fehlen.');
process.exit(1);
}
const httpsServer = https.createServer({
key: fs.readFileSync(TLS_KEY_PATH),
cert: fs.readFileSync(TLS_CERT_PATH),
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
});
wss = new WebSocketServer({ server: httpsServer });
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
httpsServer.listen(PORT, '0.0.0.0', () => {
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
});
} else {
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
}
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
View 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.

View 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();

View File

@@ -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'
);
}
};

View File

@@ -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;
`);
}
};

View File

@@ -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;
`);
}
};

View File

@@ -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;
`);
}
};

View File

@@ -0,0 +1,79 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
// falukant_data.character.reputation (integer, default random 20..80)
// Wichtig: Schema explizit angeben
// Vorgehen:
// - Spalte anlegen (falls noch nicht vorhanden)
// - bestehende Zeilen initialisieren (random 20..80)
// - DEFAULT setzen (random 20..80)
// - NOT NULL + CHECK 0..100 erzwingen
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'character'
AND column_name = 'reputation'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN reputation integer;
END IF;
END$$;
`);
// Backfill: nur NULLs initialisieren (damit bestehende Werte nicht überschrieben werden)
await queryInterface.sequelize.query(`
UPDATE falukant_data."character"
SET reputation = (floor(random()*61)+20)::int
WHERE reputation IS NULL;
`);
// DEFAULT + NOT NULL (nach Backfill)
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
ALTER COLUMN reputation SET DEFAULT (floor(random()*61)+20)::int;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
ALTER COLUMN reputation SET NOT NULL;
`);
// Enforce 0..100 at DB level (percent)
// (IF NOT EXISTS pattern, because deployments can be re-run)
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE c.conname = 'character_reputation_0_100_chk'
AND n.nspname = 'falukant_data'
AND t.relname = 'character'
) THEN
ALTER TABLE falukant_data."character"
ADD CONSTRAINT character_reputation_0_100_chk
CHECK (reputation >= 0 AND reputation <= 100);
END IF;
END$$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
DROP CONSTRAINT IF EXISTS character_reputation_0_100_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character"
DROP COLUMN IF EXISTS reputation;
`);
},
};

View File

@@ -0,0 +1,47 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Typ-Tabelle (konfigurierbar ohne Code): falukant_type.reputation_action
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_type.reputation_action (
id serial PRIMARY KEY,
tr text NOT NULL UNIQUE,
cost integer NOT NULL CHECK (cost >= 0),
base_gain integer NOT NULL CHECK (base_gain >= 0),
decay_factor double precision NOT NULL CHECK (decay_factor > 0 AND decay_factor <= 1),
min_gain integer NOT NULL DEFAULT 0 CHECK (min_gain >= 0),
decay_window_days integer NOT NULL DEFAULT 7 CHECK (decay_window_days >= 1 AND decay_window_days <= 365)
);
`);
// Log-Tabelle: falukant_log.reputation_action
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_log.reputation_action (
id serial PRIMARY KEY,
falukant_user_id integer NOT NULL,
action_type_id integer NOT NULL,
cost integer NOT NULL CHECK (cost >= 0),
base_gain integer NOT NULL CHECK (base_gain >= 0),
gain integer NOT NULL CHECK (gain >= 0),
times_used_before integer NOT NULL CHECK (times_used_before >= 0),
action_timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS reputation_action_log_user_type_idx
ON falukant_log.reputation_action (falukant_user_id, action_type_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS reputation_action_log_ts_idx
ON falukant_log.reputation_action (action_timestamp);
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_log.reputation_action;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_type.reputation_action;`);
},
};

View File

@@ -0,0 +1,46 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Für bereits existierende Installationen: Spalte sicherstellen + Backfill
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ADD COLUMN IF NOT EXISTS decay_window_days integer;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_type.reputation_action
SET decay_window_days = 7
WHERE decay_window_days IS NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ALTER COLUMN decay_window_days SET DEFAULT 7;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ALTER COLUMN decay_window_days SET NOT NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
ADD CONSTRAINT reputation_action_decay_window_days_chk
CHECK (decay_window_days >= 1 AND decay_window_days <= 365);
`);
},
async down(queryInterface, Sequelize) {
// optional: wieder entfernen
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.reputation_action
DROP COLUMN IF EXISTS decay_window_days;
`);
},
};

View File

@@ -0,0 +1,50 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Idempotentes Seed: legt Ruf-Aktionen an bzw. aktualisiert sie anhand "tr"
await queryInterface.sequelize.query(`
INSERT INTO falukant_type.reputation_action
(tr, cost, base_gain, decay_factor, min_gain, decay_window_days)
VALUES
('soup_kitchen', 500, 2, 0.85, 0, 7),
('library_donation', 5000, 4, 0.88, 0, 7),
('well_build', 8000, 4, 0.87, 0, 7),
('scholarships', 10000, 5, 0.87, 0, 7),
('church_hospice', 12000, 5, 0.87, 0, 7),
('school_funding', 15000, 6, 0.88, 0, 7),
('orphanage_build', 20000, 7, 0.90, 0, 7),
('bridge_build', 25000, 7, 0.90, 0, 7),
('hospital_donation', 30000, 8, 0.90, 0, 7),
('patronage', 40000, 9, 0.91, 0, 7),
('statue_build', 50000, 10, 0.92, 0, 7)
ON CONFLICT (tr) DO UPDATE SET
cost = EXCLUDED.cost,
base_gain = EXCLUDED.base_gain,
decay_factor = EXCLUDED.decay_factor,
min_gain = EXCLUDED.min_gain,
decay_window_days = EXCLUDED.decay_window_days;
`);
},
async down(queryInterface, Sequelize) {
// Entfernt nur die gesetzten Seeds (tr-basiert)
await queryInterface.sequelize.query(`
DELETE FROM falukant_type.reputation_action
WHERE tr IN (
'soup_kitchen',
'library_donation',
'well_build',
'scholarships',
'church_hospice',
'school_funding',
'orphanage_build',
'bridge_build',
'hospital_donation',
'patronage',
'statue_build'
);
`);
},
};

View File

@@ -0,0 +1,60 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
// Ensure column exists
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ADD COLUMN IF NOT EXISTS condition integer;
`);
// Backfill nulls (legacy data)
await queryInterface.sequelize.query(`
UPDATE falukant_data.vehicle
SET condition = 100
WHERE condition IS NULL;
`);
// Clamp out-of-range values defensively
await queryInterface.sequelize.query(`
UPDATE falukant_data.vehicle
SET condition = GREATEST(0, LEAST(100, condition))
WHERE condition < 0 OR condition > 100;
`);
// Default + NOT NULL
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ALTER COLUMN condition SET DEFAULT 100;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ALTER COLUMN condition SET NOT NULL;
`);
// Check constraint 0..100
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ADD CONSTRAINT vehicle_condition_0_100_chk
CHECK (condition >= 0 AND condition <= 100);
`);
},
async down(queryInterface, Sequelize) {
// Keep the column, but remove constraint/default to be reversible
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.vehicle
ALTER COLUMN condition DROP DEFAULT;
`);
// NOT NULL not reverted to avoid introducing NULLs on rollback; can be adjusted if needed
},
};

View File

@@ -0,0 +1,32 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
ADD COLUMN IF NOT EXISTS may_repair_vehicles boolean;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_data.director
SET may_repair_vehicles = true
WHERE may_repair_vehicles IS NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
ALTER COLUMN may_repair_vehicles SET DEFAULT true;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
ALTER COLUMN may_repair_vehicles SET NOT NULL;
`);
},
async down(queryInterface, Sequelize) {
// optional rollback: drop column
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.director
DROP COLUMN IF EXISTS may_repair_vehicles;
`);
},
};

View File

@@ -0,0 +1,61 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Sprache / Set, das geteilt werden kann
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_language (
id SERIAL PRIMARY KEY,
owner_user_id INTEGER NOT NULL,
name TEXT NOT NULL,
share_code TEXT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_language_owner_fk
FOREIGN KEY (owner_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_share_code_uniq UNIQUE (share_code)
);
`);
// Abos (Freunde)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_language_subscription (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
language_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_language_subscription_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_subscription_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_subscription_uniq UNIQUE (user_id, language_id)
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_language_owner_idx
ON community.vocab_language(owner_user_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_language_subscription_user_idx
ON community.vocab_language_subscription(user_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_language_subscription_language_idx
ON community.vocab_language_subscription(language_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language_subscription;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language;`);
}
};

View File

@@ -0,0 +1,106 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Kapitel innerhalb einer Sprache
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_chapter (
id SERIAL PRIMARY KEY,
language_id INTEGER NOT NULL,
title TEXT NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_chapter_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chapter_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chapter_language_idx
ON community.vocab_chapter(language_id);
`);
// Lexeme/Wörter (wir deduplizieren pro Sprache über normalized)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_lexeme (
id SERIAL PRIMARY KEY,
language_id INTEGER NOT NULL,
text TEXT NOT NULL,
normalized TEXT NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_lexeme_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_lexeme_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_lexeme_unique_per_language UNIQUE (language_id, normalized)
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_lexeme_language_idx
ON community.vocab_lexeme(language_id);
`);
// n:m Zuordnung pro Kapitel: Lernwort ↔ Referenzwort (Mehrdeutigkeiten möglich)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_chapter_lexeme (
id SERIAL PRIMARY KEY,
chapter_id INTEGER NOT NULL,
learning_lexeme_id INTEGER NOT NULL,
reference_lexeme_id INTEGER NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_chlex_chapter_fk
FOREIGN KEY (chapter_id)
REFERENCES community.vocab_chapter(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_learning_fk
FOREIGN KEY (learning_lexeme_id)
REFERENCES community.vocab_lexeme(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_reference_fk
FOREIGN KEY (reference_lexeme_id)
REFERENCES community.vocab_lexeme(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_unique UNIQUE (chapter_id, learning_lexeme_id, reference_lexeme_id)
);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chlex_chapter_idx
ON community.vocab_chapter_lexeme(chapter_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chlex_learning_idx
ON community.vocab_chapter_lexeme(learning_lexeme_id);
`);
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx
ON community.vocab_chapter_lexeme(reference_lexeme_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter_lexeme;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_lexeme;`);
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter;`);
}
};

View File

@@ -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;
`);
}
};

View File

@@ -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;
`);
}
};

View File

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

View File

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

View File

@@ -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;
`);
}
};

View 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';

View 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';

View File

@@ -2,6 +2,11 @@
module.exports = { module.exports = {
up: async (queryInterface, Sequelize) => { 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(` await queryInterface.sequelize.query(`
CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$ CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$
BEGIN BEGIN

View 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;

View File

@@ -95,6 +95,13 @@ import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js'; import ElectionHistory from './falukant/log/election_history.js';
import Underground from './falukant/data/underground.js'; import Underground from './falukant/data/underground.js';
import UndergroundType from './falukant/type/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 Blog from './community/blog.js';
import BlogPost from './community/blog_post.js'; import BlogPost from './community/blog_post.js';
import Campaign from './match3/campaign.js'; import Campaign from './match3/campaign.js';
@@ -284,6 +291,21 @@ export default function setupAssociations() {
RegionData.belongsTo(RegionType, { foreignKey: 'regionTypeId', as: 'regionType' }); RegionData.belongsTo(RegionType, { foreignKey: 'regionTypeId', as: 'regionType' });
RegionType.hasMany(RegionData, { foreignKey: 'regionTypeId', as: 'regions' }); 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' }); FalukantUser.belongsTo(RegionData, { foreignKey: 'mainBranchRegionId', as: 'mainBranchRegion' });
RegionData.hasMany(FalukantUser, { foreignKey: 'mainBranchRegionId', as: 'users' }); RegionData.hasMany(FalukantUser, { foreignKey: 'mainBranchRegionId', as: 'users' });
@@ -421,6 +443,89 @@ export default function setupAssociations() {
PromotionalGiftLog.belongsTo(FalukantCharacter, { foreignKey: 'recipientCharacterId', as: 'recipient' }); PromotionalGiftLog.belongsTo(FalukantCharacter, { foreignKey: 'recipientCharacterId', as: 'recipient' });
FalukantCharacter.hasMany(PromotionalGiftLog, { foreignKey: 'recipientCharacterId', as: 'giftlogs' }); 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(PromotionalGiftCharacterTrait, { foreignKey: 'gift_id', as: 'characterTraits' });
PromotionalGift.hasMany(PromotionalGiftMood, { foreignKey: 'gift_id', as: 'promotionalgiftmoods' }); PromotionalGift.hasMany(PromotionalGiftMood, { foreignKey: 'gift_id', as: 'promotionalgiftmoods' });
@@ -493,44 +598,52 @@ export default function setupAssociations() {
Learning.belongsTo(LearnRecipient, { Learning.belongsTo(LearnRecipient, {
foreignKey: 'learningRecipientId', foreignKey: 'learningRecipientId',
as: 'recipient' as: 'recipient',
constraints: false
} }
); );
LearnRecipient.hasMany(Learning, { LearnRecipient.hasMany(Learning, {
foreignKey: 'learningRecipientId', foreignKey: 'learningRecipientId',
as: 'learnings' as: 'learnings',
constraints: false
}); });
Learning.belongsTo(FalukantUser, { Learning.belongsTo(FalukantUser, {
foreignKey: 'associatedFalukantUserId', foreignKey: 'associatedFalukantUserId',
as: 'learner' as: 'learner',
constraints: false
} }
); );
FalukantUser.hasMany(Learning, { FalukantUser.hasMany(Learning, {
foreignKey: 'associatedFalukantUserId', foreignKey: 'associatedFalukantUserId',
as: 'learnings' as: 'learnings',
constraints: false
}); });
Learning.belongsTo(ProductType, { Learning.belongsTo(ProductType, {
foreignKey: 'productId', foreignKey: 'productId',
as: 'productType' as: 'productType',
constraints: false
}); });
ProductType.hasMany(Learning, { ProductType.hasMany(Learning, {
foreignKey: 'productId', foreignKey: 'productId',
as: 'learnings' as: 'learnings',
constraints: false
}); });
Learning.belongsTo(FalukantCharacter, { Learning.belongsTo(FalukantCharacter, {
foreignKey: 'associatedLearningCharacterId', foreignKey: 'associatedLearningCharacterId',
as: 'learningCharacter' as: 'learningCharacter',
constraints: false
}); });
FalukantCharacter.hasMany(Learning, { FalukantCharacter.hasMany(Learning, {
foreignKey: 'associatedLearningCharacterId', foreignKey: 'associatedLearningCharacterId',
as: 'learningsCharacter' as: 'learningsCharacter',
constraints: false
}); });
FalukantUser.hasMany(Credit, { FalukantUser.hasMany(Credit, {

View File

@@ -8,16 +8,12 @@ const Folder = sequelize.define('folder', {
allowNull: false}, allowNull: false},
parentId: { parentId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true
references: { },
model: 'folder',
key: 'id'}},
userId: { userId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: { }}, {
model: 'user',
key: 'id'}}}, {
tableName: 'folder', tableName: 'folder',
schema: 'community', schema: 'community',
underscored: true, underscored: true,

View File

@@ -10,22 +10,11 @@ const FolderImageVisibility = sequelize.define('folder_image_visibility', {
}, },
folderId: { folderId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'folder',
key: 'id'
}
}, },
visibilityTypeId: { visibilityTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: {
schema: 'type',
tableName: 'image_visibility_type'
},
key: 'id'
}
} }
}, { }, {
tableName: 'folder_image_visibility', tableName: 'folder_image_visibility',

View File

@@ -10,19 +10,11 @@ const FolderVisibilityUser = sequelize.define('folder_visibility_user', {
}, },
folderId: { folderId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'folder',
key: 'id'
}
}, },
visibilityUserId: { visibilityUserId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'image_visibility_user',
key: 'id'
}
} }
}, { }, {
tableName: 'folder_visibility_user', tableName: 'folder_visibility_user',

View File

@@ -10,19 +10,11 @@ const GuestbookEntry = sequelize.define('guestbook_entry', {
allowNull: false}, allowNull: false},
recipientId: { recipientId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: User,
key: 'id'
}
}, },
senderId: { senderId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true
references: {
model: User,
key: 'id'
}
}, },
senderUsername: { senderUsername: {
type: DataTypes.STRING, type: DataTypes.STRING,

View File

@@ -18,16 +18,12 @@ const Image = sequelize.define('image', {
unique: true}, unique: true},
folderId: { folderId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: { },
model: 'folder',
key: 'id'}},
userId: { userId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: { }}, {
model: 'user',
key: 'id'}}}, {
tableName: 'image', tableName: 'image',
schema: 'community', schema: 'community',
underscored: true, underscored: true,

View File

@@ -10,22 +10,11 @@ const ImageImageVisibility = sequelize.define('image_image_visibility', {
}, },
imageId: { imageId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'image',
key: 'id'
}
}, },
visibilityTypeId: { visibilityTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: {
schema: 'type',
tableName: 'image_visibility_type'
},
key: 'id'
}
} }
}, { }, {
tableName: 'image_image_visibility', tableName: 'image_image_visibility',

View File

@@ -7,19 +7,11 @@ import { encrypt, decrypt } from '../../utils/encryption.js';
const UserParam = sequelize.define('user_param', { const UserParam = sequelize.define('user_param', {
userId: { userId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: User,
key: 'id',
},
}, },
paramTypeId: { paramTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: UserParamType,
key: 'id',
},
}, },
value: { value: {
type: DataTypes.STRING, type: DataTypes.STRING,

View File

@@ -6,19 +6,11 @@ import UserRightType from '../type/user_right.js';
const UserRight = sequelize.define('user_right', { const UserRight = sequelize.define('user_right', {
userId: { userId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: User,
key: 'id'
}
}, },
rightTypeId: { rightTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: UserRightType,
key: 'id'
}
}}, { }}, {
tableName: 'user_right', tableName: 'user_right',
schema: 'community', schema: 'community',

View File

@@ -34,6 +34,18 @@ FalukantCharacter.init(
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
defaultValue: 1} defaultValue: 1}
,
reputation: {
type: DataTypes.INTEGER,
allowNull: false,
// Initialisierung: zufällig 20..80 (Prozent)
// DB-seitig per DEFAULT umgesetzt, damit es auch ohne App-Logic gilt.
defaultValue: sequelize.literal('(floor(random()*61)+20)'),
validate: {
min: 0,
max: 100
}
}
}, },
{ {
sequelize, sequelize,

View File

@@ -29,6 +29,10 @@ Director.init({
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: true}, defaultValue: true},
mayRepairVehicles: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true},
lastSalaryPayout: { lastSalaryPayout: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: false, allowNull: false,

View File

@@ -1,5 +1,6 @@
import { Model, DataTypes } from 'sequelize'; import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js'; import { sequelize } from '../../../utils/sequelize.js';
import WeatherType from '../type/weather.js';
class Production extends Model { } class Production extends Model { }
@@ -13,6 +14,11 @@ Production.init({
quantity: { quantity: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false}, allowNull: false},
weatherTypeId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Wetter zum Zeitpunkt der Produktionserstellung'
},
startTimestamp: { startTimestamp: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: false, allowNull: false,

View File

@@ -10,26 +10,24 @@ RegionData.init({
allowNull: false}, allowNull: false},
regionTypeId: { regionTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: RegionType,
key: 'id',
schema: 'falukant_type'
}
}, },
parentId: { parentId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true
references: {
model: 'region',
key: 'id',
schema: 'falukant_data'}
}, },
map: { map: {
type: DataTypes.JSONB, type: DataTypes.JSONB,
allowNull: true, allowNull: true,
defaultValue: {} defaultValue: {}
} }
,
taxPercent: {
type: DataTypes.DECIMAL,
allowNull: false,
defaultValue: 7,
field: 'tax_percent'
}
}, { }, {
sequelize, sequelize,
modelName: 'RegionData', modelName: 'RegionData',

View 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;

View File

@@ -8,18 +8,10 @@ Relationship.init(
{ {
character1Id: { character1Id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false},
references: {
model: FalukantCharacter,
key: 'id'},
onDelete: 'CASCADE'},
character2Id: { character2Id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false},
references: {
model: FalukantCharacter,
key: 'id'},
onDelete: 'CASCADE'},
relationshipTypeId: { relationshipTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,

View File

@@ -6,15 +6,20 @@ class FalukantStock extends Model { }
FalukantStock.init({ FalukantStock.init({
branchId: { branchId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
defaultValue: 0
}, },
stockTypeId: { stockTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false}, allowNull: false},
quantity: { quantity: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false}}, { allowNull: false},
productQuality: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Quality of the stored product (0-100)'
}
}, {
sequelize, sequelize,
modelName: 'StockData', modelName: 'StockData',
tableName: 'stock', tableName: 'stock',

View 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;

View File

@@ -8,13 +8,6 @@ FalukantUser.init({
userId: { userId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: {
model: {
tableName: 'user',
schema: 'community'
},
key: 'id'
},
unique: true}, unique: true},
money: { money: {
type: DataTypes.DECIMAL(10, 2), type: DataTypes.DECIMAL(10, 2),
@@ -38,12 +31,11 @@ FalukantUser.init({
defaultValue: 1}, defaultValue: 1},
mainBranchRegionId: { mainBranchRegionId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true
references: { },
model: RegionData, lastNobilityAdvanceAt: {
key: 'id', type: DataTypes.DATE,
schema: 'falukant_data' allowNull: true
}
} }
}, { }, {
sequelize, sequelize,

View File

@@ -26,13 +26,11 @@ UserHouse.init({
}, },
houseTypeId: { houseTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
defaultValue: 1
}, },
userId: { userId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
defaultValue: 1
} }
}, { }, {
sequelize, sequelize,

View 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 (0100)
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;

View 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;

View File

@@ -1,7 +1,12 @@
import { Model, DataTypes } from 'sequelize'; import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js'; 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({ Notification.init({
userId: { userId: {
@@ -10,6 +15,11 @@ Notification.init({
tr: { tr: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false}, allowNull: false},
character_name: {
type: DataTypes.STRING,
allowNull: true,
field: 'character_name'
},
shown: { shown: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,

View File

@@ -0,0 +1,59 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ReputationActionLog extends Model {}
ReputationActionLog.init(
{
falukantUserId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'falukant_user_id',
},
actionTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'action_type_id',
},
cost: {
type: DataTypes.INTEGER,
allowNull: false,
},
baseGain: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'base_gain',
},
gain: {
type: DataTypes.INTEGER,
allowNull: false,
},
timesUsedBefore: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'times_used_before',
},
actionTimestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP'),
field: 'action_timestamp',
},
},
{
sequelize,
modelName: 'ReputationActionLog',
tableName: 'reputation_action',
schema: 'falukant_log',
timestamps: false,
underscored: true,
indexes: [
{ fields: ['falukant_user_id', 'action_type_id'] },
{ fields: ['action_timestamp'] },
],
}
);
export default ReputationActionLog;

View File

@@ -10,13 +10,11 @@ PromotionalGiftCharacterTrait.init(
giftId: { giftId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'gift_id', field: 'gift_id',
references: { model: PromotionalGift, key: 'id' },
allowNull: false allowNull: false
}, },
traitId: { traitId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'trait_id', field: 'trait_id',
references: { model: CharacterTrait, key: 'id' },
allowNull: false allowNull: false
}, },
suitability: { suitability: {

View File

@@ -10,19 +10,11 @@ PromotionalGiftMood.init(
giftId: { giftId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'gift_id', field: 'gift_id',
references: {
model: PromotionalGift,
key: 'id'
},
allowNull: false allowNull: false
}, },
moodId: { moodId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'mood_id', field: 'mood_id',
references: {
model: Mood,
key: 'id'
},
allowNull: false allowNull: false
}, },
suitability: { suitability: {

View 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;

View File

@@ -9,11 +9,7 @@ RegionType.init({
allowNull: false}, allowNull: false},
parentId: { parentId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true
references: {
model: 'region',
key: 'id',
schema: 'falukant_type'}
} }
}, { }, {
sequelize, sequelize,

View File

@@ -0,0 +1,51 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ReputationActionType extends Model {}
ReputationActionType.init(
{
tr: {
type: DataTypes.STRING,
allowNull: false,
},
cost: {
type: DataTypes.INTEGER,
allowNull: false,
},
baseGain: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'base_gain',
},
decayFactor: {
type: DataTypes.FLOAT,
allowNull: false,
field: 'decay_factor',
},
minGain: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'min_gain',
},
decayWindowDays: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 7,
field: 'decay_window_days',
},
},
{
sequelize,
modelName: 'ReputationActionType',
tableName: 'reputation_action',
schema: 'falukant_type',
timestamps: false,
underscored: true,
}
);
export default ReputationActionType;

View 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;

View 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;

View File

@@ -4,19 +4,11 @@ import { DataTypes } from 'sequelize';
const ForumForumPermission = sequelize.define('forum_forum_permission', { const ForumForumPermission = sequelize.define('forum_forum_permission', {
forumId: { forumId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'forum',
key: 'id'
}
}, },
permissionId: { permissionId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'forum_permission',
key: 'id'
}
} }
}, { }, {
tableName: 'forum_forum_permission', tableName: 'forum_forum_permission',

View File

@@ -79,6 +79,8 @@ import Party from './falukant/data/party.js';
import MusicType from './falukant/type/music.js'; import MusicType from './falukant/type/music.js';
import BanquetteType from './falukant/type/banquette.js'; import BanquetteType from './falukant/type/banquette.js';
import PartyInvitedNobility from './falukant/data/partyInvitedNobility.js'; import PartyInvitedNobility from './falukant/data/partyInvitedNobility.js';
import ReputationActionType from './falukant/type/reputation_action.js';
import ReputationActionLog from './falukant/log/reputation_action.js';
import ChildRelation from './falukant/data/child_relation.js'; import ChildRelation from './falukant/data/child_relation.js';
import LearnRecipient from './falukant/type/learn_recipient.js'; import LearnRecipient from './falukant/type/learn_recipient.js';
import Learning from './falukant/data/learning.js'; import Learning from './falukant/data/learning.js';
@@ -113,6 +115,13 @@ import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js'; import ElectionHistory from './falukant/log/election_history.js';
import UndergroundType from './falukant/type/underground.js'; import UndergroundType from './falukant/type/underground.js';
import Underground from './falukant/data/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 Room from './chat/room.js';
import ChatUser from './chat/user.js'; import ChatUser from './chat/user.js';
@@ -201,12 +210,18 @@ const models = {
BanquetteType, BanquetteType,
Party, Party,
PartyInvitedNobility, PartyInvitedNobility,
ReputationActionType,
ReputationActionLog,
ChildRelation, ChildRelation,
LearnRecipient, LearnRecipient,
Learning, Learning,
Credit, Credit,
DebtorsPrism, DebtorsPrism,
HealthActivity, HealthActivity,
RegionDistance,
VehicleType,
Vehicle,
Transport,
PoliticalOfficeType, PoliticalOfficeType,
PoliticalOfficeRequirement, PoliticalOfficeRequirement,
PoliticalOfficeBenefitType, PoliticalOfficeBenefitType,
@@ -220,6 +235,9 @@ const models = {
ElectionHistory, ElectionHistory,
UndergroundType, UndergroundType,
Underground, Underground,
WeatherType,
Weather,
ProductWeatherEffect,
Room, Room,
ChatUser, ChatUser,
ChatRight, ChatRight,

View File

@@ -9,11 +9,7 @@ const Match3Level = sequelize.define('Match3Level', {
}, },
campaignId: { campaignId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'match3_campaigns',
key: 'id'
}
}, },
name: { name: {
type: DataTypes.STRING(255), type: DataTypes.STRING(255),

View File

@@ -10,19 +10,11 @@ const Match3LevelTileType = sequelize.define('Match3LevelTileType', {
levelId: { levelId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: {
model: 'match3_levels',
key: 'id'
},
comment: 'Referenz auf den Level' comment: 'Referenz auf den Level'
}, },
tileTypeId: { tileTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: {
model: 'match3_tile_types',
key: 'id'
},
comment: 'Referenz auf den Tile-Typ' comment: 'Referenz auf den Tile-Typ'
}, },
weight: { weight: {

View File

@@ -9,14 +9,7 @@ const TaxiHighscore = sequelize.define('TaxiHighscore', {
}, },
userId: { userId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, // Kann null sein, falls User gelöscht wird allowNull: true // Kann null sein, falls User gelöscht wird
references: {
model: {
tableName: 'user',
schema: 'community'
},
key: 'id'
}
}, },
nickname: { nickname: {
type: DataTypes.STRING(100), type: DataTypes.STRING(100),
@@ -44,13 +37,6 @@ const TaxiHighscore = sequelize.define('TaxiHighscore', {
mapId: { mapId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
references: {
model: {
tableName: 'taxi_map',
schema: 'taxi'
},
key: 'id'
},
comment: 'ID der gespielten Map' comment: 'ID der gespielten Map'
}, },
mapName: { mapName: {

View File

@@ -378,10 +378,13 @@ export async function createTriggers() {
tp.election_id, tp.election_id,
tp.tp_office_type_id, tp.tp_office_type_id,
tp.tp_election_date, tp.tp_election_date,
( COALESCE(
SELECT json_agg(vr) (
FROM votes vr SELECT json_agg(vr)
WHERE vr.election_id = tp.election_id 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 created_at,
NOW() AS updated_at NOW() AS updated_at

View File

@@ -13,13 +13,7 @@ const interestTranslation = sequelize.define('interest_translation_type', {
}, },
interestsId: { interestsId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: Interest,
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}}, { }}, {
tableName: 'interest_translation', tableName: 'interest_translation',
schema: 'type', schema: 'type',

View File

@@ -21,11 +21,7 @@ const UserParamType = sequelize.define('user_param_type', {
}, },
settingsId: { settingsId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false
references: {
model: 'settings',
key: 'id'
}
}, },
orderId: { orderId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,

View File

@@ -7,7 +7,7 @@ const UserRightType = sequelize.define('user_right_type', {
allowNull: false allowNull: false
} }
}, { }, {
tableName: 'user_right_type', tableName: 'user_right',
schema: 'type', schema: 'type',
underscored: true underscored: true
}); });

1726
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "NODE_ENV=development node server.js", "dev": "NODE_ENV=development node server.js",
"start-daemon": "node daemonServer.js",
"sync-db": "node sync-database.js", "sync-db": "node sync-database.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
@@ -15,7 +16,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"amqplib": "^0.10.4", "amqplib": "^0.10.4",
"bcrypt": "^5.1.1", "bcryptjs": "^2.4.3",
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@@ -26,9 +27,9 @@
"i18n": "^0.15.1", "i18n": "^0.15.1",
"joi": "^17.13.3", "joi": "^17.13.3",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"multer": "^1.4.5-lts.1", "multer": "^2.0.0",
"mysql2": "^3.10.3", "mysql2": "^3.10.3",
"nodemailer": "^6.9.14", "nodemailer": "^7.0.11",
"pg": "^8.12.0", "pg": "^8.12.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"redis": "^4.7.0", "redis": "^4.7.0",

View File

@@ -18,6 +18,7 @@ router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom);
// --- Users Admin --- // --- Users Admin ---
router.get('/users/search', authenticate, adminController.searchUsers); router.get('/users/search', authenticate, adminController.searchUsers);
router.get('/users/statistics', authenticate, adminController.getUserStatistics); router.get('/users/statistics', authenticate, adminController.getUserStatistics);
router.get('/users/batch', authenticate, adminController.getUsers);
router.get('/users/:id', authenticate, adminController.getUser); router.get('/users/:id', authenticate, adminController.getUser);
router.put('/users/:id', authenticate, adminController.updateUser); router.put('/users/:id', authenticate, adminController.updateUser);
@@ -40,6 +41,14 @@ router.get('/falukant/branches/:falukantUserId', authenticate, adminController.g
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock); router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
router.post('/falukant/stock', authenticate, adminController.addFalukantStock); router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
router.get('/falukant/stock-types', authenticate, adminController.getFalukantStockTypes); 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);
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
// --- Minigames Admin --- // --- Minigames Admin ---
router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns); router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns);

View File

@@ -15,6 +15,7 @@ router.get('/branches/types', falukantController.getBranchTypes);
router.get('/branches/:branch', falukantController.getBranch); router.get('/branches/:branch', falukantController.getBranch);
router.get('/branches', falukantController.getBranches); router.get('/branches', falukantController.getBranches);
router.post('/branches', falukantController.createBranch); router.post('/branches', falukantController.createBranch);
router.post('/branches/upgrade', falukantController.upgradeBranch);
router.get('/productions', falukantController.getAllProductions); router.get('/productions', falukantController.getAllProductions);
router.post('/production', falukantController.createProduction); router.post('/production', falukantController.createProduction);
router.get('/production/:branchId', falukantController.getProduction); router.get('/production/:branchId', falukantController.getProduction);
@@ -37,6 +38,9 @@ router.get('/director/:branchId', falukantController.getDirectorForBranch);
router.get('/directors', falukantController.getAllDirectors); router.get('/directors', falukantController.getAllDirectors);
router.post('/directors', falukantController.updateDirector); router.post('/directors', falukantController.updateDirector);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal); router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.post('/family/set-heir', falukantController.setHeir);
router.get('/heirs/potential', falukantController.getPotentialHeirs);
router.post('/heirs/select', falukantController.selectHeir);
router.get('/family/gifts', falukantController.getGifts); router.get('/family/gifts', falukantController.getGifts);
router.get('/family/children', falukantController.getChildren); router.get('/family/children', falukantController.getChildren);
router.post('/family/gift', falukantController.sendGift); router.post('/family/gift', falukantController.sendGift);
@@ -51,6 +55,8 @@ router.post('/houses', falukantController.buyUserHouse);
router.get('/party/types', falukantController.getPartyTypes); router.get('/party/types', falukantController.getPartyTypes);
router.post('/party', falukantController.createParty); router.post('/party', falukantController.createParty);
router.get('/party', falukantController.getParties); router.get('/party', falukantController.getParties);
router.get('/reputation/actions', falukantController.getReputationActions);
router.post('/reputation/actions', falukantController.executeReputationAction);
router.get('/family/notbaptised', falukantController.getNotBaptisedChildren); router.get('/family/notbaptised', falukantController.getNotBaptisedChildren);
router.post('/church/baptise', falukantController.baptise); router.post('/church/baptise', falukantController.baptise);
router.get('/education', falukantController.getEducation); router.get('/education', falukantController.getEducation);
@@ -69,6 +75,18 @@ router.post('/politics/elections', falukantController.vote);
router.get('/politics/open', falukantController.getOpenPolitics); router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections); router.post('/politics/open', falukantController.applyForElections);
router.get('/cities', falukantController.getRegions); router.get('/cities', falukantController.getRegions);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-region-batch', falukantController.getProductPricesInRegionBatch);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes);
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('/underground/types', falukantController.getUndergroundTypes);
router.get('/notifications', falukantController.getNotifications); router.get('/notifications', falukantController.getNotifications);
router.get('/notifications/all', falukantController.getAllNotifications); router.get('/notifications/all', falukantController.getAllNotifications);

View 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;

View File

@@ -0,0 +1,27 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import VocabController from '../controllers/vocabController.js';
const router = express.Router();
const vocabController = new VocabController();
router.use(authenticate);
router.get('/languages', vocabController.listLanguages);
router.post('/languages', vocabController.createLanguage);
router.post('/subscribe', vocabController.subscribe);
router.get('/languages/:languageId', vocabController.getLanguage);
// Kapitel
router.get('/languages/:languageId/chapters', vocabController.listChapters);
router.post('/languages/:languageId/chapters', vocabController.createChapter);
router.get('/languages/:languageId/vocabs', vocabController.listLanguageVocabs);
router.get('/languages/:languageId/search', vocabController.searchVocabs);
router.get('/chapters/:chapterId', vocabController.getChapter);
router.get('/chapters/:chapterId/vocabs', vocabController.listChapterVocabs);
router.post('/chapters/:chapterId/vocabs', vocabController.addVocabToChapter);
export default router;

View File

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

View File

@@ -10,7 +10,7 @@ import UserParamType from "../models/type/user_param.js";
import ContactMessage from "../models/service/contactmessage.js"; import ContactMessage from "../models/service/contactmessage.js";
import ContactService from "./ContactService.js"; import ContactService from "./ContactService.js";
import { sendAnswerEmail } from './emailService.js'; import { sendAnswerEmail } from './emailService.js';
import { Op } from 'sequelize'; import { Op, Sequelize } from 'sequelize';
import FalukantUser from "../models/falukant/data/user.js"; import FalukantUser from "../models/falukant/data/user.js";
import FalukantCharacter from "../models/falukant/data/character.js"; import FalukantCharacter from "../models/falukant/data/character.js";
import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js"; import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js";
@@ -19,9 +19,15 @@ import Branch from "../models/falukant/data/branch.js";
import FalukantStock from "../models/falukant/data/stock.js"; import FalukantStock from "../models/falukant/data/stock.js";
import FalukantStockType from "../models/falukant/type/stock.js"; import FalukantStockType from "../models/falukant/type/stock.js";
import RegionData from "../models/falukant/data/region.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 BranchType from "../models/falukant/type/branch.js";
import RegionDistance from "../models/falukant/data/region_distance.js";
import Room from '../models/chat/room.js'; import Room from '../models/chat/room.js';
import UserParam from '../models/community/user_param.js'; import UserParam from '../models/community/user_param.js';
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
import { sequelize } from '../utils/sequelize.js';
import npcCreationJobService from './npcCreationJobService.js';
import { v4 as uuidv4 } from 'uuid';
class AdminService { class AdminService {
async hasUserAccess(userId, section) { async hasUserAccess(userId, section) {
@@ -298,6 +304,115 @@ 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 getTitlesOfNobility(userId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const titles = await TitleOfNobility.findAll({
order: [['id', 'ASC']],
attributes: ['id', 'labelTr', 'level']
});
return titles;
}
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) { async updateFalukantStock(userId, stockId, quantity) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) { if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess'); throw new Error('noaccess');
@@ -441,6 +556,30 @@ class AdminService {
return { id: user.hashedId, username: user.username, active: user.active, registrationDate: user.registrationDate }; 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) { async updateUser(requestingHashedUserId, targetHashedId, data) {
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) { if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
throw new Error('noaccess'); throw new Error('noaccess');
@@ -961,6 +1100,216 @@ class AdminService {
ageGroups ageGroups
}; };
} }
async createNPCs(userId, options) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const {
regionIds, // Array von Region-IDs oder null für alle Städte
minAge, // Mindestalter in Jahren
maxAge, // Maximalalter in Jahren
minTitleId, // Minimale Title-ID
maxTitleId, // Maximale Title-ID
count // Anzahl der zu erstellenden NPCs
} = options;
// Berechne zuerst die Gesamtanzahl, um den Job richtig zu initialisieren
// WICHTIG: Nur Städte (city) verwenden, keine anderen Region-Typen
let targetRegions = [];
if (regionIds && regionIds.length > 0) {
targetRegions = await RegionData.findAll({
where: {
id: { [Op.in]: regionIds }
},
include: [{
model: RegionType,
as: 'regionType',
where: { labelTr: 'city' },
required: true // INNER JOIN - nur Regionen mit city-Type
}]
});
} else {
targetRegions = await RegionData.findAll({
include: [{
model: RegionType,
as: 'regionType',
where: { labelTr: 'city' },
required: true // INNER JOIN - nur Regionen mit city-Type
}]
});
}
// Zusätzliche Sicherheit: Filtere explizit nach city-Type
targetRegions = targetRegions.filter(region => {
return region.regionType && region.regionType.labelTr === 'city';
});
console.log(`[createNPCs] Found ${targetRegions.length} cities (filtered)`);
if (targetRegions.length > 0) {
console.log(`[createNPCs] City names: ${targetRegions.map(r => r.name).join(', ')}`);
}
if (targetRegions.length === 0) {
throw new Error('No cities found');
}
const titles = await TitleOfNobility.findAll({
where: {
id: {
[Op.between]: [minTitleId, maxTitleId]
}
},
order: [['id', 'ASC']]
});
if (titles.length === 0) {
throw new Error('No titles found in specified range');
}
const totalNPCs = targetRegions.length * titles.length * count;
// Erstelle Job-ID
const jobId = uuidv4();
npcCreationJobService.createJob(userId, jobId);
npcCreationJobService.updateProgress(jobId, 0, totalNPCs);
npcCreationJobService.setStatus(jobId, 'running');
// Starte asynchronen Prozess
this._createNPCsAsync(jobId, userId, {
regionIds,
minAge,
maxAge,
minTitleId,
maxTitleId,
count,
targetRegions,
titles
}).catch(error => {
console.error('Error in _createNPCsAsync:', error);
const errorMessage = error?.message || error?.toString() || 'Unknown error occurred';
npcCreationJobService.setError(jobId, errorMessage);
});
return { jobId };
}
async _createNPCsAsync(jobId, userId, options) {
try {
const {
regionIds,
minAge,
maxAge,
minTitleId,
maxTitleId,
count,
targetRegions,
titles
} = options;
const genders = ['male', 'female'];
const createdNPCs = [];
const totalNPCs = targetRegions.length * titles.length * count;
let currentNPC = 0;
console.log(`[NPC Creation Job ${jobId}] Starting creation of ${totalNPCs} NPCs`);
// Erstelle NPCs in einer Transaktion
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
await sequelize.transaction(async (t) => {
for (const region of targetRegions) {
for (const title of titles) {
// Erstelle 'count' NPCs für diese Stadt-Titel-Kombination
for (let i = 0; i < count; i++) {
// Zufälliges Geschlecht
const gender = genders[Math.floor(Math.random() * genders.length)];
// Zufälliger Vorname
const firstName = await FalukantPredefineFirstname.findAll({
where: { gender },
order: sequelize.fn('RANDOM'),
limit: 1,
transaction: t
});
if (firstName.length === 0) {
throw new Error(`No first names found for gender: ${gender}`);
}
const fnObj = firstName[0];
// Zufälliger Nachname
const lastName = await FalukantPredefineLastname.findAll({
order: sequelize.fn('RANDOM'),
limit: 1,
transaction: t
});
if (lastName.length === 0) {
throw new Error('No last names found');
}
const lnObj = lastName[0];
// Zufälliges Alter (in Jahren, wird in Tage umgerechnet)
const randomAge = Math.floor(Math.random() * (maxAge - minAge + 1)) + minAge;
const birthdate = new Date();
birthdate.setDate(birthdate.getDate() - randomAge); // 5 Tage = 5 Jahre alt
// Erstelle den NPC-Charakter (ohne userId = NPC)
const npc = await FalukantCharacter.create({
userId: null, // Wichtig: null = NPC
regionId: region.id,
firstName: fnObj.id,
lastName: lnObj.id,
gender: gender,
birthdate: birthdate,
titleOfNobility: title.id,
health: 100,
moodId: 1
}, { transaction: t });
createdNPCs.push({
id: npc.id,
firstName: fnObj.name,
lastName: lnObj.name,
gender: gender,
age: randomAge,
region: region.name,
title: title.labelTr
});
// Update Progress
currentNPC++;
npcCreationJobService.updateProgress(jobId, currentNPC, totalNPCs);
}
}
}
});
console.log(`[NPC Creation Job ${jobId}] Completed: ${createdNPCs.length} NPCs created`);
// Job abschließen
npcCreationJobService.setResult(jobId, {
success: true,
count: createdNPCs.length,
countPerCombination: count,
totalCombinations: targetRegions.length * titles.length,
npcs: createdNPCs
});
} catch (error) {
console.error(`[NPC Creation Job ${jobId}] Error:`, error);
throw error; // Re-throw für den catch-Block in createNPCs
}
}
getNPCsCreationStatus(userId, jobId) {
const job = npcCreationJobService.getJob(jobId);
if (!job) {
throw new Error('Job not found');
}
if (job.userId !== userId) {
throw new Error('Access denied');
}
return job;
}
} }
export default new AdminService(); export default new AdminService();

View File

@@ -1,4 +1,4 @@
import bcrypt from 'bcrypt'; import bcrypt from 'bcryptjs';
import crypto from 'crypto'; import crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import User from '../models/community/user.js'; import User from '../models/community/user.js';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
// In-Memory Job-Status-Service für NPC-Erstellung
// Für Produktion sollte man Redis oder eine Datenbank verwenden
const jobs = new Map();
class NPCCreationJobService {
createJob(userId, jobId) {
jobs.set(jobId, {
userId,
status: 'pending',
progress: 0,
total: 0,
current: 0,
startTime: Date.now(),
estimatedTimeRemaining: null,
error: null,
result: null
});
return jobId;
}
getJob(jobId) {
return jobs.get(jobId);
}
updateProgress(jobId, current, total) {
const job = jobs.get(jobId);
if (!job) return;
job.current = current;
job.total = total;
job.progress = total > 0 ? Math.round((current / total) * 100) : 0;
// Berechne verbleibende Zeit basierend auf bisheriger Geschwindigkeit
if (current > 0 && job.progress < 100) {
const elapsed = Date.now() - job.startTime;
const avgTimePerItem = elapsed / current;
const remaining = total - current;
job.estimatedTimeRemaining = Math.round(remaining * avgTimePerItem);
}
}
setStatus(jobId, status) {
const job = jobs.get(jobId);
if (!job) return;
job.status = status;
}
setError(jobId, error) {
const job = jobs.get(jobId);
if (!job) return;
job.status = 'error';
job.error = error;
}
setResult(jobId, result) {
const job = jobs.get(jobId);
if (!job) return;
job.status = 'completed';
job.result = result;
job.progress = 100;
job.estimatedTimeRemaining = 0;
}
deleteJob(jobId) {
jobs.delete(jobId);
}
// Cleanup alte Jobs (älter als 1 Stunde)
cleanupOldJobs() {
const oneHourAgo = Date.now() - (60 * 60 * 1000);
for (const [jobId, job] of jobs.entries()) {
if (job.startTime < oneHourAgo) {
jobs.delete(jobId);
}
}
}
}
// Cleanup alle 10 Minuten
setInterval(() => {
const service = new NPCCreationJobService();
service.cleanupOldJobs();
}, 10 * 60 * 1000);
export default new NPCCreationJobService();

View File

@@ -328,7 +328,7 @@ class SettingsService extends BaseService{
} }
// Verify old password // Verify old password
const bcrypt = await import('bcrypt'); const bcrypt = await import('bcryptjs');
const match = await bcrypt.compare(settings.oldpassword, user.password); const match = await bcrypt.compare(settings.oldpassword, user.password);
if (!match) { if (!match) {
throw new Error('Old password is incorrect'); throw new Error('Old password is incorrect');

View File

@@ -0,0 +1,532 @@
import crypto from 'crypto';
import User from '../models/community/user.js';
import { sequelize } from '../utils/sequelize.js';
import { notifyUser } from '../utils/socket.js';
export default class VocabService {
async _getUserByHashedId(hashedUserId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
const err = new Error('User not found');
err.status = 404;
throw err;
}
return user;
}
_normalizeLexeme(text) {
return String(text || '')
.trim()
.toLowerCase()
.replace(/\s+/g, ' ');
}
async _getLanguageAccess(userId, languageId) {
const id = Number.parseInt(languageId, 10);
if (!Number.isFinite(id)) {
const err = new Error('Invalid language id');
err.status = 400;
throw err;
}
const [row] = await sequelize.query(
`
SELECT
l.id,
(l.owner_user_id = :userId) AS "isOwner"
FROM community.vocab_language l
WHERE l.id = :languageId
AND (
l.owner_user_id = :userId
OR EXISTS (
SELECT 1
FROM community.vocab_language_subscription s
WHERE s.user_id = :userId AND s.language_id = l.id
)
)
LIMIT 1
`,
{
replacements: { userId, languageId: id },
type: sequelize.QueryTypes.SELECT,
}
);
if (!row) {
const err = new Error('Language not found or no access');
err.status = 404;
throw err;
}
return row;
}
async _getChapterAccess(userId, chapterId) {
const id = Number.parseInt(chapterId, 10);
if (!Number.isFinite(id)) {
const err = new Error('Invalid chapter id');
err.status = 400;
throw err;
}
const [row] = await sequelize.query(
`
SELECT
c.id,
c.language_id AS "languageId",
c.title,
(l.owner_user_id = :userId) AS "isOwner"
FROM community.vocab_chapter c
JOIN community.vocab_language l ON l.id = c.language_id
WHERE c.id = :chapterId
AND (
l.owner_user_id = :userId
OR EXISTS (
SELECT 1
FROM community.vocab_language_subscription s
WHERE s.user_id = :userId AND s.language_id = l.id
)
)
LIMIT 1
`,
{
replacements: { userId, chapterId: id },
type: sequelize.QueryTypes.SELECT,
}
);
if (!row) {
const err = new Error('Chapter not found or no access');
err.status = 404;
throw err;
}
return row;
}
async listLanguages(hashedUserId) {
const user = await this._getUserByHashedId(hashedUserId);
const rows = await sequelize.query(
`
SELECT
l.id,
l.name,
l.share_code AS "shareCode",
TRUE AS "isOwner"
FROM community.vocab_language l
WHERE l.owner_user_id = :userId
UNION ALL
SELECT
l.id,
l.name,
NULL::text AS "shareCode",
FALSE AS "isOwner"
FROM community.vocab_language_subscription s
JOIN community.vocab_language l ON l.id = s.language_id
WHERE s.user_id = :userId
ORDER BY name ASC
`,
{
replacements: { userId: user.id },
type: sequelize.QueryTypes.SELECT,
}
);
return { languages: rows };
}
async listLanguagesForMenu(userId) {
// userId ist die numerische community.user.id
const rows = await sequelize.query(
`
SELECT l.id, l.name
FROM community.vocab_language l
WHERE l.owner_user_id = :userId
UNION
SELECT l.id, l.name
FROM community.vocab_language_subscription s
JOIN community.vocab_language l ON l.id = s.language_id
WHERE s.user_id = :userId
ORDER BY name ASC
`,
{
replacements: { userId },
type: sequelize.QueryTypes.SELECT,
}
);
return rows;
}
async createLanguage(hashedUserId, { name }) {
const user = await this._getUserByHashedId(hashedUserId);
const cleanName = typeof name === 'string' ? name.trim() : '';
if (!cleanName || cleanName.length < 2 || cleanName.length > 60) {
const err = new Error('Invalid language name');
err.status = 400;
throw err;
}
// 16 hex chars => ausreichend kurz, gut teilbar
const shareCode = crypto.randomBytes(8).toString('hex');
const [created] = await sequelize.query(
`
INSERT INTO community.vocab_language (owner_user_id, name, share_code)
VALUES (:ownerUserId, :name, :shareCode)
RETURNING id, name, share_code AS "shareCode"
`,
{
replacements: { ownerUserId: user.id, name: cleanName, shareCode },
type: sequelize.QueryTypes.SELECT,
}
);
// Menü dynamisch nachladen (bei allen offenen Tabs/Clients)
try {
notifyUser(user.hashedId, 'reloadmenu', {});
} catch (_) {}
return created;
}
async subscribeByShareCode(hashedUserId, { shareCode }) {
const user = await this._getUserByHashedId(hashedUserId);
const code = typeof shareCode === 'string' ? shareCode.trim() : '';
if (!code || code.length < 6 || code.length > 128) {
const err = new Error('Invalid share code');
err.status = 400;
throw err;
}
const [lang] = await sequelize.query(
`
SELECT id, owner_user_id AS "ownerUserId", name
FROM community.vocab_language
WHERE share_code = :shareCode
LIMIT 1
`,
{
replacements: { shareCode: code },
type: sequelize.QueryTypes.SELECT,
}
);
if (!lang) {
const err = new Error('Language not found');
err.status = 404;
throw err;
}
// Owner braucht kein Abo
if (lang.ownerUserId === user.id) {
return { subscribed: false, message: 'Already owner', languageId: lang.id };
}
await sequelize.query(
`
INSERT INTO community.vocab_language_subscription (user_id, language_id)
VALUES (:userId, :languageId)
ON CONFLICT (user_id, language_id) DO NOTHING
`,
{
replacements: { userId: user.id, languageId: lang.id },
type: sequelize.QueryTypes.INSERT,
}
);
try {
notifyUser(user.hashedId, 'reloadmenu', {});
} catch (_) {}
return { subscribed: true, languageId: lang.id, name: lang.name };
}
async getLanguage(hashedUserId, languageId) {
const user = await this._getUserByHashedId(hashedUserId);
const id = Number.parseInt(languageId, 10);
if (!Number.isFinite(id)) {
const err = new Error('Invalid language id');
err.status = 400;
throw err;
}
const [row] = await sequelize.query(
`
SELECT
l.id,
l.name,
CASE WHEN l.owner_user_id = :userId THEN l.share_code ELSE NULL END AS "shareCode",
(l.owner_user_id = :userId) AS "isOwner"
FROM community.vocab_language l
WHERE l.id = :languageId
AND (
l.owner_user_id = :userId
OR EXISTS (
SELECT 1
FROM community.vocab_language_subscription s
WHERE s.user_id = :userId AND s.language_id = l.id
)
)
LIMIT 1
`,
{
replacements: { userId: user.id, languageId: id },
type: sequelize.QueryTypes.SELECT,
}
);
if (!row) {
const err = new Error('Language not found or no access');
err.status = 404;
throw err;
}
return row;
}
async listChapters(hashedUserId, languageId) {
const user = await this._getUserByHashedId(hashedUserId);
const access = await this._getLanguageAccess(user.id, languageId);
const rows = await sequelize.query(
`
SELECT
c.id,
c.title,
c.created_at AS "createdAt",
(
SELECT COUNT(*)
FROM community.vocab_chapter_lexeme cl
WHERE cl.chapter_id = c.id
)::int AS "vocabCount"
FROM community.vocab_chapter c
WHERE c.language_id = :languageId
ORDER BY c.title ASC
`,
{
replacements: { languageId: access.id },
type: sequelize.QueryTypes.SELECT,
}
);
return { chapters: rows, isOwner: access.isOwner };
}
async createChapter(hashedUserId, languageId, { title }) {
const user = await this._getUserByHashedId(hashedUserId);
const access = await this._getLanguageAccess(user.id, languageId);
if (!access.isOwner) {
const err = new Error('Only owner can create chapters');
err.status = 403;
throw err;
}
const cleanTitle = typeof title === 'string' ? title.trim() : '';
if (!cleanTitle || cleanTitle.length < 2 || cleanTitle.length > 80) {
const err = new Error('Invalid chapter title');
err.status = 400;
throw err;
}
const [created] = await sequelize.query(
`
INSERT INTO community.vocab_chapter (language_id, title, created_by_user_id)
VALUES (:languageId, :title, :userId)
RETURNING id, title, created_at AS "createdAt"
`,
{
replacements: { languageId: access.id, title: cleanTitle, userId: user.id },
type: sequelize.QueryTypes.SELECT,
}
);
return created;
}
async getChapter(hashedUserId, chapterId) {
const user = await this._getUserByHashedId(hashedUserId);
const ch = await this._getChapterAccess(user.id, chapterId);
return { id: ch.id, languageId: ch.languageId, title: ch.title, isOwner: ch.isOwner };
}
async listChapterVocabs(hashedUserId, chapterId) {
const user = await this._getUserByHashedId(hashedUserId);
const ch = await this._getChapterAccess(user.id, chapterId);
const rows = await sequelize.query(
`
SELECT
cl.id,
l1.text AS "learning",
l2.text AS "reference",
cl.created_at AS "createdAt"
FROM community.vocab_chapter_lexeme cl
JOIN community.vocab_lexeme l1 ON l1.id = cl.learning_lexeme_id
JOIN community.vocab_lexeme l2 ON l2.id = cl.reference_lexeme_id
WHERE cl.chapter_id = :chapterId
ORDER BY l1.text ASC, l2.text ASC
`,
{
replacements: { chapterId: ch.id },
type: sequelize.QueryTypes.SELECT,
}
);
return { chapter: { id: ch.id, title: ch.title, languageId: ch.languageId, isOwner: ch.isOwner }, vocabs: rows };
}
async listLanguageVocabs(hashedUserId, languageId) {
const user = await this._getUserByHashedId(hashedUserId);
const access = await this._getLanguageAccess(user.id, languageId);
const rows = await sequelize.query(
`
SELECT
cl.id,
c.id AS "chapterId",
c.title AS "chapterTitle",
l1.text AS "learning",
l2.text AS "reference",
cl.created_at AS "createdAt"
FROM community.vocab_chapter_lexeme cl
JOIN community.vocab_chapter c ON c.id = cl.chapter_id
JOIN community.vocab_lexeme l1 ON l1.id = cl.learning_lexeme_id
JOIN community.vocab_lexeme l2 ON l2.id = cl.reference_lexeme_id
WHERE c.language_id = :languageId
ORDER BY c.title ASC, l1.text ASC, l2.text ASC
`,
{
replacements: { languageId: access.id },
type: sequelize.QueryTypes.SELECT,
}
);
return { languageId: access.id, isOwner: access.isOwner, vocabs: rows };
}
async searchVocabs(hashedUserId, languageId, { q = '', learning = '', motherTongue = '' } = {}) {
const user = await this._getUserByHashedId(hashedUserId);
const access = await this._getLanguageAccess(user.id, languageId);
const query = typeof q === 'string' ? q.trim() : '';
// Abwärtskompatibel: falls alte Parameter genutzt werden, zusammenfassen
const learningTerm = typeof learning === 'string' ? learning.trim() : '';
const motherTerm = typeof motherTongue === 'string' ? motherTongue.trim() : '';
const effective = query || learningTerm || motherTerm;
if (!effective) {
const err = new Error('Missing search term');
err.status = 400;
throw err;
}
const like = `%${effective}%`;
const rows = await sequelize.query(
`
SELECT
cl.id,
c.id AS "chapterId",
c.title AS "chapterTitle",
l1.text AS "learning",
l2.text AS "motherTongue"
FROM community.vocab_chapter_lexeme cl
JOIN community.vocab_chapter c ON c.id = cl.chapter_id
JOIN community.vocab_lexeme l1 ON l1.id = cl.learning_lexeme_id
JOIN community.vocab_lexeme l2 ON l2.id = cl.reference_lexeme_id
WHERE c.language_id = :languageId
AND (l1.text ILIKE :like OR l2.text ILIKE :like)
ORDER BY l2.text ASC, l1.text ASC, c.title ASC
LIMIT 200
`,
{
replacements: {
languageId: access.id,
like,
},
type: sequelize.QueryTypes.SELECT,
}
);
return { languageId: access.id, results: rows };
}
async addVocabToChapter(hashedUserId, chapterId, { learning, reference }) {
const user = await this._getUserByHashedId(hashedUserId);
const ch = await this._getChapterAccess(user.id, chapterId);
if (!ch.isOwner) {
const err = new Error('Only owner can add vocab');
err.status = 403;
throw err;
}
const learningText = typeof learning === 'string' ? learning.trim() : '';
const referenceText = typeof reference === 'string' ? reference.trim() : '';
if (!learningText || !referenceText) {
const err = new Error('Invalid vocab');
err.status = 400;
throw err;
}
const learningNorm = this._normalizeLexeme(learningText);
const referenceNorm = this._normalizeLexeme(referenceText);
// Transaktion: Lexeme upserten + Zuordnung setzen
return await sequelize.transaction(async (t) => {
const [learningLex] = await sequelize.query(
`
INSERT INTO community.vocab_lexeme (language_id, text, normalized, created_by_user_id)
VALUES (:languageId, :text, :normalized, :userId)
ON CONFLICT (language_id, normalized) DO UPDATE SET text = EXCLUDED.text
RETURNING id
`,
{
replacements: { languageId: ch.languageId, text: learningText, normalized: learningNorm, userId: user.id },
type: sequelize.QueryTypes.SELECT,
transaction: t,
}
);
const [referenceLex] = await sequelize.query(
`
INSERT INTO community.vocab_lexeme (language_id, text, normalized, created_by_user_id)
VALUES (:languageId, :text, :normalized, :userId)
ON CONFLICT (language_id, normalized) DO UPDATE SET text = EXCLUDED.text
RETURNING id
`,
{
replacements: { languageId: ch.languageId, text: referenceText, normalized: referenceNorm, userId: user.id },
type: sequelize.QueryTypes.SELECT,
transaction: t,
}
);
const [mapping] = await sequelize.query(
`
INSERT INTO community.vocab_chapter_lexeme (chapter_id, learning_lexeme_id, reference_lexeme_id, created_by_user_id)
VALUES (:chapterId, :learningId, :referenceId, :userId)
ON CONFLICT (chapter_id, learning_lexeme_id, reference_lexeme_id) DO NOTHING
RETURNING id
`,
{
replacements: {
chapterId: ch.id,
learningId: learningLex.id,
referenceId: referenceLex.id,
userId: user.id,
},
type: sequelize.QueryTypes.SELECT,
transaction: t,
}
);
return { created: Boolean(mapping?.id) };
});
}
}

View 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;

View 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

View 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

View 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;

View 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.

View File

@@ -228,10 +228,29 @@ async function initializeFalukantStockTypes() {
} }
async function initializeFalukantProducts() { 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: 'wheat', category: 1, productionTime: 2, sellCost: 7 },
{ labelTr: 'grain', 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: 'fish', category: 1, productionTime: 2, sellCost: 7 },
{ labelTr: 'meat', category: 1, productionTime: 2, sellCost: 7 }, { labelTr: 'meat', category: 1, productionTime: 2, sellCost: 7 },
{ labelTr: 'leather', 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: 'shield', category: 4, productionTime: 5, sellCost: 60 },
{ labelTr: 'horse', category: 5, productionTime: 5, sellCost: 60 }, { labelTr: 'horse', category: 5, productionTime: 5, sellCost: 60 },
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 }, { labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 },
], { ];
const productsToInsert = baseProducts;
await ProductType.bulkCreate(productsToInsert, {
ignoreDuplicates: true, ignoreDuplicates: true,
}); });
} }

View File

@@ -12,11 +12,17 @@ import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js";
import PartyType from "../../models/falukant/type/party.js"; import PartyType from "../../models/falukant/type/party.js";
import MusicType from "../../models/falukant/type/music.js"; import MusicType from "../../models/falukant/type/music.js";
import BanquetteType from "../../models/falukant/type/banquette.js"; import BanquetteType from "../../models/falukant/type/banquette.js";
import ReputationActionType from "../../models/falukant/type/reputation_action.js";
import VehicleType from "../../models/falukant/type/vehicle.js";
import LearnRecipient from "../../models/falukant/type/learn_recipient.js"; import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js"; import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js"; import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js"; import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
import UndergroundType from "../../models/falukant/type/underground.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. // Debug-Flag: Nur wenn DEBUG_FALUKANT=1 gesetzt ist, werden ausführliche Logs ausgegeben.
const falukantDebug = process.env.DEBUG_FALUKANT === '1'; const falukantDebug = process.env.DEBUG_FALUKANT === '1';
@@ -36,13 +42,67 @@ export const initializeFalukantTypes = async () => {
await initializeFalukantPartyTypes(); await initializeFalukantPartyTypes();
await initializeFalukantMusicTypes(); await initializeFalukantMusicTypes();
await initializeFalukantBanquetteTypes(); await initializeFalukantBanquetteTypes();
await initializeFalukantReputationActions();
await initializeLearnerTypes(); await initializeLearnerTypes();
await initializePoliticalOfficeBenefitTypes(); await initializePoliticalOfficeBenefitTypes();
await initializePoliticalOfficeTypes(); await initializePoliticalOfficeTypes();
await initializePoliticalOfficePrerequisites(); await initializePoliticalOfficePrerequisites();
await initializeUndergroundTypes(); await initializeUndergroundTypes();
await initializeVehicleTypes();
await initializeFalukantWeatherTypes();
await initializeFalukantWeathers();
await initializeFalukantProductWeatherEffects();
}; };
const reputationActions = [
// Günstig / häufig: schnelle Abnutzung
{ tr: "soup_kitchen", cost: 500, baseGain: 2, decayFactor: 0.85, minGain: 0, decayWindowDays: 7 },
// Mittel: gesellschaftlich anerkannt
{ tr: "library_donation", cost: 5000, baseGain: 4, decayFactor: 0.88, minGain: 0, decayWindowDays: 7 },
{ tr: "scholarships", cost: 10000, baseGain: 5, decayFactor: 0.87, minGain: 0, decayWindowDays: 7 },
{ tr: "church_hospice", cost: 12000, baseGain: 5, decayFactor: 0.87, minGain: 0, decayWindowDays: 7 },
{ tr: "school_funding", cost: 15000, baseGain: 6, decayFactor: 0.88, minGain: 0, decayWindowDays: 7 },
// Großprojekte: teurer, langsamerer Rufverfall
{ tr: "orphanage_build", cost: 20000, baseGain: 7, decayFactor: 0.90, minGain: 0, decayWindowDays: 7 },
{ tr: "bridge_build", cost: 25000, baseGain: 7, decayFactor: 0.90, minGain: 0, decayWindowDays: 7 },
{ tr: "hospital_donation", cost: 30000, baseGain: 8, decayFactor: 0.90, minGain: 0, decayWindowDays: 7 },
{ tr: "patronage", cost: 40000, baseGain: 9, decayFactor: 0.91, minGain: 0, decayWindowDays: 7 },
{ tr: "statue_build", cost: 50000, baseGain: 10, decayFactor: 0.92, minGain: 0, decayWindowDays: 7 },
{ tr: "well_build", cost: 8000, baseGain: 4, decayFactor: 0.87, minGain: 0, decayWindowDays: 7 },
];
async function initializeFalukantReputationActions() {
// robustes Upsert ohne FK/Constraints-Ärger:
// - finde existierende nach tr
// - update bei Änderungen
// - create wenn fehlt
const existing = await ReputationActionType.findAll({ attributes: ['id', 'tr', 'cost', 'baseGain', 'decayFactor', 'minGain'] });
const existingByTr = new Map(existing.map(e => [e.tr, e]));
for (const a of reputationActions) {
const found = existingByTr.get(a.tr);
if (!found) {
await ReputationActionType.create(a);
continue;
}
const needsUpdate =
Number(found.cost) !== Number(a.cost) ||
Number(found.baseGain) !== Number(a.baseGain) ||
Number(found.decayFactor) !== Number(a.decayFactor) ||
Number(found.minGain) !== Number(a.minGain) ||
Number(found.decayWindowDays || 7) !== Number(a.decayWindowDays || 7);
if (needsUpdate) {
await found.update({
cost: a.cost,
baseGain: a.baseGain,
decayFactor: a.decayFactor,
minGain: a.minGain,
decayWindowDays: a.decayWindowDays ?? 7,
});
}
}
}
const regionTypes = []; const regionTypes = [];
const regionTypeTrs = [ const regionTypeTrs = [
"country", "country",
@@ -273,6 +333,17 @@ const learnerTypes = [
{ tr: 'director', }, { 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 = [ const politicalOfficeBenefitTypes = [
{ tr: 'salary' }, { tr: 'salary' },
{ tr: 'reputation' }, { tr: 'reputation' },
@@ -282,6 +353,7 @@ const politicalOfficeBenefitTypes = [
{ tr: 'tax_exemption' }, { tr: 'tax_exemption' },
{ tr: 'guard_protection' }, { tr: 'guard_protection' },
{ tr: 'court_immunity' }, { tr: 'court_immunity' },
{ tr: 'set_regionl_tax' },
]; ];
const politicalOffices = [ const politicalOffices = [
@@ -883,6 +955,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 () => { export const initializePoliticalOfficeBenefitTypes = async () => {
for (const benefitType of politicalOfficeBenefitTypes) { for (const benefitType of politicalOfficeBenefitTypes) {
await PoliticalOfficeBenefitType.findOrCreate({ await PoliticalOfficeBenefitType.findOrCreate({
@@ -972,3 +1069,445 @@ export const initializeFalukantTitlesOfNobility = async () => {
throw error; 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;
}
};

View File

@@ -3,6 +3,25 @@ import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
// Optionales Performance-Logging (aktivierbar per ENV)
// - SQL_BENCHMARK=1: Sequelize liefert Query-Timing (ms) an logger
// - SQL_SLOW_MS=200: Logge nur Queries ab dieser Dauer (wenn SQL_LOG_ALL nicht gesetzt)
// - SQL_LOG_ALL=1: Logge alle Queries (auch ohne benchmark)
const SQL_BENCHMARK = process.env.SQL_BENCHMARK === '1';
const SQL_LOG_ALL = process.env.SQL_LOG_ALL === '1';
const SQL_SLOW_MS = Number.parseInt(process.env.SQL_SLOW_MS || '200', 10);
const sqlLogger = (sql, timing) => {
// Sequelize ruft logging(sql) oder logging(sql, timing) abhängig von benchmark auf.
if (!SQL_BENCHMARK) {
if (SQL_LOG_ALL) console.log(sql);
return;
}
const ms = typeof timing === 'number' ? timing : 0;
if (SQL_LOG_ALL || ms >= SQL_SLOW_MS) {
console.log(`🛢️ SQL ${ms}ms: ${sql}`);
}
};
// Validiere Umgebungsvariablen // Validiere Umgebungsvariablen
const dbName = process.env.DB_NAME; const dbName = process.env.DB_NAME;
const dbUser = process.env.DB_USER; const dbUser = process.env.DB_USER;
@@ -26,6 +45,8 @@ const sequelize = new Sequelize(dbName, dbUser, dbPass, {
timestamps: false, timestamps: false,
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
}, },
benchmark: SQL_BENCHMARK,
logging: sqlLogger,
}); });
const createSchemas = async () => { const createSchemas = async () => {
@@ -45,6 +66,16 @@ const createSchemas = async () => {
const initializeDatabase = async () => { const initializeDatabase = async () => {
await createSchemas(); 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. // Modelle nur laden, aber an dieser Stelle NICHT syncen.
// Das Syncing (inkl. alter: true bei Bedarf) wird anschließend zentral // Das Syncing (inkl. alter: true bei Bedarf) wird anschließend zentral
// über syncModelsWithUpdates()/syncModelsAlways gesteuert. // über syncModelsWithUpdates()/syncModelsAlways gesteuert.
@@ -95,7 +126,8 @@ const syncModelsWithUpdates = async (models) => {
if (needsUpdate) { if (needsUpdate) {
console.log('🔄 Schema-Updates nötig - verwende alter: true'); console.log('🔄 Schema-Updates nötig - verwende alter: true');
for (const model of Object.values(models)) { 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'); console.log('✅ Schema-Updates abgeschlossen');
} else { } else {
@@ -363,12 +395,13 @@ const getExpectedDefaultValue = (defaultValue) => {
const updateSchema = async (models) => { const updateSchema = async (models) => {
console.log('🔄 Aktualisiere Datenbankschema...'); console.log('🔄 Aktualisiere Datenbankschema...');
for (const model of Object.values(models)) { 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'); console.log('✅ Datenbankschema aktualisiert');
}; };
async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null) { async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null, transaction = null) {
try { try {
const result = await sequelize.query( const result = await sequelize.query(
`SELECT falukant_data.update_money( `SELECT falukant_data.update_money(
@@ -385,6 +418,7 @@ async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, ch
changedBy, changedBy,
}, },
type: sequelize.QueryTypes.SELECT, type: sequelize.QueryTypes.SELECT,
transaction: transaction || undefined,
} }
); );
return { return {
@@ -407,7 +441,310 @@ const syncModelsAlways = async (models) => {
try { try {
for (const model of Object.values(models)) { for (const model of Object.values(models)) {
await model.sync({ alter: true, force: false }); // 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'); console.log('✅ Schema-Updates für alle Models abgeschlossen');
} catch (error) { } catch (error) {

View File

@@ -33,6 +33,123 @@ const syncDatabase = async () => {
console.log("Initializing database schemas..."); console.log("Initializing database schemas...");
await initializeDatabase(); await initializeDatabase();
// Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations)
// Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt.
// Damit API/Menu nicht mit "relation does not exist" (42P01) scheitert, legen wir die Tabellen idempotent an.
console.log("Ensuring Vocab-Trainer tables exist...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_language (
id SERIAL PRIMARY KEY,
owner_user_id INTEGER NOT NULL,
name TEXT NOT NULL,
share_code TEXT NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_language_owner_fk
FOREIGN KEY (owner_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_share_code_uniq UNIQUE (share_code)
);
CREATE TABLE IF NOT EXISTS community.vocab_language_subscription (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
language_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_language_subscription_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_subscription_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_language_subscription_uniq UNIQUE (user_id, language_id)
);
CREATE INDEX IF NOT EXISTS vocab_language_owner_idx
ON community.vocab_language(owner_user_id);
CREATE INDEX IF NOT EXISTS vocab_language_subscription_user_idx
ON community.vocab_language_subscription(user_id);
CREATE INDEX IF NOT EXISTS vocab_language_subscription_language_idx
ON community.vocab_language_subscription(language_id);
CREATE TABLE IF NOT EXISTS community.vocab_chapter (
id SERIAL PRIMARY KEY,
language_id INTEGER NOT NULL,
title TEXT NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_chapter_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chapter_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS vocab_chapter_language_idx
ON community.vocab_chapter(language_id);
CREATE TABLE IF NOT EXISTS community.vocab_lexeme (
id SERIAL PRIMARY KEY,
language_id INTEGER NOT NULL,
text TEXT NOT NULL,
normalized TEXT NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_lexeme_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_lexeme_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_lexeme_unique_per_language UNIQUE (language_id, normalized)
);
CREATE INDEX IF NOT EXISTS vocab_lexeme_language_idx
ON community.vocab_lexeme(language_id);
CREATE TABLE IF NOT EXISTS community.vocab_chapter_lexeme (
id SERIAL PRIMARY KEY,
chapter_id INTEGER NOT NULL,
learning_lexeme_id INTEGER NOT NULL,
reference_lexeme_id INTEGER NOT NULL,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_chlex_chapter_fk
FOREIGN KEY (chapter_id)
REFERENCES community.vocab_chapter(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_learning_fk
FOREIGN KEY (learning_lexeme_id)
REFERENCES community.vocab_lexeme(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_reference_fk
FOREIGN KEY (reference_lexeme_id)
REFERENCES community.vocab_lexeme(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_chlex_unique UNIQUE (chapter_id, learning_lexeme_id, reference_lexeme_id)
);
CREATE INDEX IF NOT EXISTS vocab_chlex_chapter_idx
ON community.vocab_chapter_lexeme(chapter_id);
CREATE INDEX IF NOT EXISTS vocab_chlex_learning_idx
ON community.vocab_chapter_lexeme(learning_lexeme_id);
CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx
ON community.vocab_chapter_lexeme(reference_lexeme_id);
`);
console.log("✅ Vocab-Trainer Tabellen sind vorhanden.");
} catch (e) {
console.warn('⚠️ Konnte Vocab-Trainer Tabellen nicht sicherstellen:', e?.message || e);
}
// Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt // Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt
console.log("Pre-ensure Taxi columns (traffic_light) ..."); console.log("Pre-ensure Taxi columns (traffic_light) ...");
try { try {
@@ -54,6 +171,112 @@ const syncDatabase = async () => {
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e); 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..."); console.log("Setting up associations...");
setupAssociations(); setupAssociations();
@@ -104,6 +327,10 @@ const syncDatabase = async () => {
// Deployment-Synchronisation (immer Schema-Updates) // Deployment-Synchronisation (immer Schema-Updates)
const syncDatabaseForDeployment = async () => { const syncDatabaseForDeployment = async () => {
try { 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 // Zeige den aktuellen Stage an
const currentStage = process.env.STAGE || 'nicht gesetzt'; const currentStage = process.env.STAGE || 'nicht gesetzt';
console.log(`🚀 Starte Datenbank-Synchronisation für Deployment (Stage: ${currentStage})`); console.log(`🚀 Starte Datenbank-Synchronisation für Deployment (Stage: ${currentStage})`);
@@ -133,6 +360,185 @@ const syncDatabaseForDeployment = async () => {
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e); 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`);
}
// Cleanup vehicle.condition: Legacy-Nulls + Range clamp (UI zeigt sonst "Unbekannt")
const result9 = await sequelize.query(`
UPDATE falukant_data.vehicle
SET condition = 100
WHERE condition IS NULL;
`);
const updatedNullConditions = result9[1] || 0;
if (updatedNullConditions > 0) {
console.log(`${updatedNullConditions} vehicle.condition NULL → 100 gesetzt`);
}
const result10 = await sequelize.query(`
UPDATE falukant_data.vehicle
SET condition = GREATEST(0, LEAST(100, condition))
WHERE condition < 0 OR condition > 100;
`);
const clampedConditions = result10[1] || 0;
if (clampedConditions > 0) {
console.log(`${clampedConditions} vehicle.condition Werte auf 0..100 geklemmt`);
}
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0 && updatedNullConditions === 0 && clampedConditions === 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..."); console.log("Setting up associations...");
setupAssociations(); setupAssociations();

78
build-local.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
# YourPart Daemon Local Build Script für OpenSUSE Tumbleweed
# Führen Sie dieses Script lokal auf Ihrem Entwicklungsrechner aus
set -euo pipefail
# Farben für Output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_info "Starte lokalen Build für YourPart Daemon auf OpenSUSE Tumbleweed..."
# Prüfe ob wir im richtigen Verzeichnis sind
if [ ! -f "CMakeLists.txt" ] || [ ! -f "daemon.conf" ]; then
log_error "Bitte führen Sie dieses Script aus dem Projektverzeichnis aus!"
exit 1
fi
# Prüfe Dependencies
log_info "Prüfe Dependencies..."
if ! command -v cmake &> /dev/null; then
log_error "CMake nicht gefunden. Führen Sie zuerst install-dependencies-opensuse.sh aus!"
exit 1
fi
if ! command -v g++ &> /dev/null; then
log_error "G++ nicht gefunden. Führen Sie zuerst install-dependencies-opensuse.sh aus!"
exit 1
fi
# Erstelle Build-Verzeichnis
log_info "Erstelle Build-Verzeichnis..."
if [ ! -d "build" ]; then
mkdir build
fi
cd build
# Konfiguriere CMake
log_info "Konfiguriere CMake..."
cmake .. -DCMAKE_BUILD_TYPE=Release
# Kompiliere
log_info "Kompiliere Projekt..."
make -j$(nproc)
cd ..
log_success "Lokaler Build abgeschlossen!"
log_info ""
log_info "Build-Ergebnisse:"
log_info "- Binärdatei: build/yourpart-daemon"
log_info "- Größe: $(du -h build/yourpart-daemon | cut -f1)"
log_info ""
log_info "Nächste Schritte:"
log_info "1. Testen Sie die Binärdatei lokal"
log_info "2. Deployen Sie auf den Server mit deploy.sh"
log_info "3. Oder verwenden Sie deploy-server.sh direkt auf dem Server"

Some files were not shown because too many files have changed in this diff Show More