Compare commits

127 Commits
taxi ... main

Author SHA1 Message Date
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
171 changed files with 11367 additions and 2861 deletions

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

@@ -16,6 +16,7 @@ import match3Router from './routers/match3Router.js';
import taxiRouter from './routers/taxiRouter.js';
import taxiMapRouter from './routers/taxiMapRouter.js';
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
import termineRouter from './routers/termineRouter.js';
import cors from 'cors';
import './jobs/sessionCleanup.js';
@@ -52,6 +53,7 @@ app.use('/api/forum', forumRouter);
app.use('/api/falukant', falukantRouter);
app.use('/api/friendships', friendshipRouter);
app.use('/api/blog', blogRouter);
app.use('/api/termine', termineRouter);
// Serve frontend SPA for non-API routes to support history mode clean URLs
const frontendDir = path.join(__dirname, '../frontend');

View File

@@ -27,6 +27,7 @@ class AdminController {
// User administration
this.searchUsers = this.searchUsers.bind(this);
this.getUser = this.getUser.bind(this);
this.getUsers = this.getUsers.bind(this);
this.updateUser = this.updateUser.bind(this);
// Rights
@@ -37,6 +38,11 @@ class AdminController {
// Statistics
this.getUserStatistics = this.getUserStatistics.bind(this);
this.getFalukantRegions = this.getFalukantRegions.bind(this);
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
this.getRegionDistances = this.getRegionDistances.bind(this);
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
}
async getOpenInterests(req, res) {
@@ -74,6 +80,30 @@ class AdminController {
}
}
async getUsers(req, res) {
try {
const { userid: requester } = req.headers;
let { ids } = req.query;
if (!ids) {
return res.status(400).json({ error: 'ids query parameter is required' });
}
// Unterstütze sowohl Array-Format (ids[]=...) als auch komma-separierten String (ids=...)
let hashedIds;
if (Array.isArray(ids)) {
hashedIds = ids;
} else if (typeof ids === 'string') {
hashedIds = ids.split(',').map(id => id.trim()).filter(id => id.length > 0);
} else {
return res.status(400).json({ error: 'ids must be an array or comma-separated string' });
}
const result = await AdminService.getUsersByHashedIds(requester, hashedIds);
res.status(200).json(result);
} catch (error) {
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async updateUser(req, res) {
try {
const { userid: requester } = req.headers;
@@ -290,6 +320,69 @@ class AdminController {
}
}
async getFalukantRegions(req, res) {
try {
const { userid: userId } = req.headers;
const regions = await AdminService.getFalukantRegions(userId);
res.status(200).json(regions);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async updateFalukantRegionMap(req, res) {
try {
const { userid: userId } = req.headers;
const { id } = req.params;
const { map } = req.body || {};
const region = await AdminService.updateFalukantRegionMap(userId, id, map);
res.status(200).json(region);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : (error.message === 'regionNotFound' ? 404 : 500);
res.status(status).json({ error: error.message });
}
}
async getRegionDistances(req, res) {
try {
const { userid: userId } = req.headers;
const distances = await AdminService.getRegionDistances(userId);
res.status(200).json(distances);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async upsertRegionDistance(req, res) {
try {
const { userid: userId } = req.headers;
const record = await AdminService.upsertRegionDistance(userId, req.body || {});
res.status(200).json(record);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 400;
res.status(status).json({ error: error.message });
}
}
async deleteRegionDistance(req, res) {
try {
const { userid: userId } = req.headers;
const { id } = req.params;
const result = await AdminService.deleteRegionDistance(userId, id);
res.status(200).json(result);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : (error.message === 'notfound' ? 404 : 500);
res.status(status).json({ error: error.message });
}
}
async getRoomTypes(req, res) {
try {
const userId = req.headers.userid;

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.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
this.createProduction = this._wrapWithUser((userId, req) => {
const { branchId, productId, quantity } = req.body;
return this.service.createProduction(userId, branchId, productId, quantity);
@@ -91,6 +92,7 @@ class FalukantController {
if (!result) throw { status: 404, message: 'No family data found' };
return result;
});
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId);
@@ -143,6 +145,24 @@ class FalukantController {
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
this.getProductPriceInRegion = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const regionId = parseInt(req.query.regionId, 10);
if (Number.isNaN(productId) || Number.isNaN(regionId)) {
throw new Error('productId and regionId are required');
}
return this.service.getProductPriceInRegion(userId, productId, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice);
const currentRegionId = req.query.currentRegionId ? parseInt(req.query.currentRegionId, 10) : null;
if (Number.isNaN(productId) || Number.isNaN(currentPrice)) {
throw new Error('productId and currentPrice are required');
}
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
});
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
@@ -181,6 +201,33 @@ class FalukantController {
});
});
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
this.buyVehicles = this._wrapWithUser(
(userId, req) => this.service.buyVehicles(userId, req.body),
{ successStatus: 201 }
);
this.getVehicles = this._wrapWithUser(
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
);
this.createTransport = this._wrapWithUser(
(userId, req) => this.service.createTransport(userId, req.body),
{ successStatus: 201 }
);
this.getTransportRoute = this._wrapWithUser(
(userId, req) => this.service.getTransportRoute(userId, req.query)
);
this.getBranchTransports = this._wrapWithUser(
(userId, req) => this.service.getBranchTransports(userId, req.params.branchId)
);
this.repairVehicle = this._wrapWithUser(
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
{ successStatus: 200 }
);
this.repairAllVehicles = this._wrapWithUser(
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
{ successStatus: 200 }
);
}

View File

@@ -117,10 +117,6 @@ const menuStructure = {
visible: ["hasfalukantaccount"],
path: "/falukant/branch"
},
directors: {
visible: ["hasfalukantaccount"],
path: "/falukant/directors"
},
family: {
visible: ["hasfalukantaccount"],
path: "/falukant/family"
@@ -251,6 +247,10 @@ const menuStructure = {
visible: ["mainadmin", "chatrooms"],
path: "/admin/chatrooms"
},
servicesStatus: {
visible: ["mainadmin"],
path: "/admin/services/status"
},
interests: {
visible: ["mainadmin", "interests"],
path: "/admin/interests"
@@ -270,6 +270,10 @@ const menuStructure = {
visible: ["mainadmin", "falukant"],
path: "/admin/falukant/database"
},
mapEditor: {
visible: ["mainadmin", "falukant"],
path: "/admin/falukant/map"
},
}
},
minigames: {

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

98
backend/daemonServer.js Normal file
View File

@@ -0,0 +1,98 @@
import WebSocket, { WebSocketServer } from 'ws';
const PORT = 4551;
// Einfache In-Memory-Struktur für Verbindungen (für spätere Erweiterungen)
const connections = new Set();
function createServer() {
const wss = new WebSocketServer({ port: PORT });
console.log(`[Daemon] WebSocket-Server startet auf Port ${PORT} ...`);
wss.on('connection', (ws, req) => {
const peer = req.socket.remoteAddress + ':' + req.socket.remotePort;
ws.isAlive = true;
ws.userId = null;
connections.add(ws);
console.log(`[Daemon] Neue Verbindung von ${peer}`);
ws.on('message', (message) => {
try {
if (message.toString() === 'pong') {
// Client-Pong für unser Ping
ws.isAlive = true;
return;
}
const data = JSON.parse(message.toString());
// Vom Frontend gesendet nach Verbindungsaufbau
if (data.event === 'setUserId' && data.data?.userId) {
ws.userId = data.data.userId;
console.log(`[Daemon] setUserId erhalten: ${ws.userId}`);
return;
}
// Admin-Dialog: WebSocket-Log anfordern
if (data.event === 'getWebsocketLog') {
const response = {
event: 'getWebsocketLogResponse',
entries: [] // aktuell keine Log-Historie implementiert
};
ws.send(JSON.stringify(response));
return;
}
// Platzhalter für spätere Events
// console.log('[Daemon] Unbekanntes Event:', data);
} catch (err) {
console.error('[Daemon] Fehler beim Verarbeiten einer Nachricht:', err);
}
});
ws.on('close', () => {
connections.delete(ws);
console.log('[Daemon] Verbindung geschlossen');
});
ws.on('error', (err) => {
console.error('[Daemon] WebSocket-Fehler (Verbindung):', err);
});
});
// Einfache Ping/Pong-Mechanik, damit Verbindungen sauber erkannt werden
const interval = setInterval(() => {
for (const ws of connections) {
if (ws.isAlive === false) {
console.log('[Daemon] Verbindung wegen fehlendem Pong beendet');
ws.terminate();
connections.delete(ws);
continue;
}
ws.isAlive = false;
try {
ws.send('ping');
} catch (err) {
console.error('[Daemon] Fehler beim Senden von Ping:', err);
}
}
}, 30000);
wss.on('close', () => {
clearInterval(interval);
connections.clear();
console.log('[Daemon] Server gestoppt');
});
wss.on('error', (err) => {
console.error('[Daemon] Server-Fehler:', err);
});
return wss;
}
createServer();

7
backend/data/termine.csv Normal file
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,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,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 = {
up: async (queryInterface, Sequelize) => {
// Aktiviere die pgcrypto Erweiterung, die die digest() Funktion bereitstellt
await queryInterface.sequelize.query(`
CREATE EXTENSION IF NOT EXISTS pgcrypto;
`);
await queryInterface.sequelize.query(`
CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$
BEGIN

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 Underground from './falukant/data/underground.js';
import UndergroundType from './falukant/type/underground.js';
import VehicleType from './falukant/type/vehicle.js';
import Vehicle from './falukant/data/vehicle.js';
import Transport from './falukant/data/transport.js';
import RegionDistance from './falukant/data/region_distance.js';
import WeatherType from './falukant/type/weather.js';
import Weather from './falukant/data/weather.js';
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
import Blog from './community/blog.js';
import BlogPost from './community/blog_post.js';
import Campaign from './match3/campaign.js';
@@ -284,6 +291,21 @@ export default function setupAssociations() {
RegionData.belongsTo(RegionType, { foreignKey: 'regionTypeId', as: 'regionType' });
RegionType.hasMany(RegionData, { foreignKey: 'regionTypeId', as: 'regions' });
Weather.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasOne(Weather, { foreignKey: 'regionId', as: 'weather' });
Weather.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
WeatherType.hasMany(Weather, { foreignKey: 'weatherTypeId', as: 'weathers' });
ProductWeatherEffect.belongsTo(ProductType, { foreignKey: 'productId', as: 'product' });
ProductType.hasMany(ProductWeatherEffect, { foreignKey: 'productId', as: 'weatherEffects' });
ProductWeatherEffect.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
WeatherType.hasMany(ProductWeatherEffect, { foreignKey: 'weatherTypeId', as: 'productEffects' });
Production.belongsTo(WeatherType, { foreignKey: 'weatherTypeId', as: 'weatherType' });
WeatherType.hasMany(Production, { foreignKey: 'weatherTypeId', as: 'productions' });
FalukantUser.belongsTo(RegionData, { foreignKey: 'mainBranchRegionId', as: 'mainBranchRegion' });
RegionData.hasMany(FalukantUser, { foreignKey: 'mainBranchRegionId', as: 'users' });
@@ -421,6 +443,89 @@ export default function setupAssociations() {
PromotionalGiftLog.belongsTo(FalukantCharacter, { foreignKey: 'recipientCharacterId', as: 'recipient' });
FalukantCharacter.hasMany(PromotionalGiftLog, { foreignKey: 'recipientCharacterId', as: 'giftlogs' });
// Vehicles & Transports
VehicleType.hasMany(Vehicle, {
foreignKey: 'vehicleTypeId',
as: 'vehicles',
});
Vehicle.belongsTo(VehicleType, {
foreignKey: 'vehicleTypeId',
as: 'type',
});
FalukantUser.hasMany(Vehicle, {
foreignKey: 'falukantUserId',
as: 'vehicles',
});
Vehicle.belongsTo(FalukantUser, {
foreignKey: 'falukantUserId',
as: 'owner',
});
RegionData.hasMany(Vehicle, {
foreignKey: 'regionId',
as: 'vehicles',
});
Vehicle.belongsTo(RegionData, {
foreignKey: 'regionId',
as: 'region',
});
// Region distances
RegionData.hasMany(RegionDistance, {
foreignKey: 'sourceRegionId',
as: 'distancesFrom',
});
RegionData.hasMany(RegionDistance, {
foreignKey: 'targetRegionId',
as: 'distancesTo',
});
RegionDistance.belongsTo(RegionData, {
foreignKey: 'sourceRegionId',
as: 'sourceRegion',
});
RegionDistance.belongsTo(RegionData, {
foreignKey: 'targetRegionId',
as: 'targetRegion',
});
Transport.belongsTo(RegionData, {
foreignKey: 'sourceRegionId',
as: 'sourceRegion',
});
Transport.belongsTo(RegionData, {
foreignKey: 'targetRegionId',
as: 'targetRegion',
});
RegionData.hasMany(Transport, {
foreignKey: 'sourceRegionId',
as: 'outgoingTransports',
});
RegionData.hasMany(Transport, {
foreignKey: 'targetRegionId',
as: 'incomingTransports',
});
Transport.belongsTo(ProductType, {
foreignKey: 'productId',
as: 'productType',
});
ProductType.hasMany(Transport, {
foreignKey: 'productId',
as: 'transports',
});
Transport.belongsTo(Vehicle, {
foreignKey: 'vehicleId',
as: 'vehicle',
});
Vehicle.hasMany(Transport, {
foreignKey: 'vehicleId',
as: 'transports',
});
PromotionalGift.hasMany(PromotionalGiftCharacterTrait, { foreignKey: 'gift_id', as: 'characterTraits' });
PromotionalGift.hasMany(PromotionalGiftMood, { foreignKey: 'gift_id', as: 'promotionalgiftmoods' });
@@ -493,44 +598,52 @@ export default function setupAssociations() {
Learning.belongsTo(LearnRecipient, {
foreignKey: 'learningRecipientId',
as: 'recipient'
as: 'recipient',
constraints: false
}
);
LearnRecipient.hasMany(Learning, {
foreignKey: 'learningRecipientId',
as: 'learnings'
as: 'learnings',
constraints: false
});
Learning.belongsTo(FalukantUser, {
foreignKey: 'associatedFalukantUserId',
as: 'learner'
as: 'learner',
constraints: false
}
);
FalukantUser.hasMany(Learning, {
foreignKey: 'associatedFalukantUserId',
as: 'learnings'
as: 'learnings',
constraints: false
});
Learning.belongsTo(ProductType, {
foreignKey: 'productId',
as: 'productType'
as: 'productType',
constraints: false
});
ProductType.hasMany(Learning, {
foreignKey: 'productId',
as: 'learnings'
as: 'learnings',
constraints: false
});
Learning.belongsTo(FalukantCharacter, {
foreignKey: 'associatedLearningCharacterId',
as: 'learningCharacter'
as: 'learningCharacter',
constraints: false
});
FalukantCharacter.hasMany(Learning, {
foreignKey: 'associatedLearningCharacterId',
as: 'learningsCharacter'
as: 'learningsCharacter',
constraints: false
});
FalukantUser.hasMany(Credit, {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: FalukantCharacter,
key: 'id'},
onDelete: 'CASCADE'},
allowNull: false},
character2Id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: FalukantCharacter,
key: 'id'},
onDelete: 'CASCADE'},
allowNull: false},
relationshipTypeId: {
type: DataTypes.INTEGER,
allowNull: false,

View File

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

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

View File

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

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 { sequelize } from '../../../utils/sequelize.js';
class Notification extends Model { }
class Notification extends Model {
// Getter für characterName - wird nicht synchronisiert, da es kein Datenbankfeld ist
get characterName() {
return this.getDataValue('character_name') || null;
}
}
Notification.init({
userId: {
@@ -10,6 +15,11 @@ Notification.init({
tr: {
type: DataTypes.STRING,
allowNull: false},
character_name: {
type: DataTypes.STRING,
allowNull: true,
field: 'character_name'
},
shown: {
type: DataTypes.BOOLEAN,
allowNull: false,

View File

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

View File

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

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},
parentId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: 'region',
key: 'id',
schema: 'falukant_type'}
allowNull: true
}
}, {
sequelize,

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', {
forumId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'forum',
key: 'id'
}
allowNull: false
},
permissionId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'forum_permission',
key: 'id'
}
allowNull: false
}
}, {
tableName: 'forum_forum_permission',

View File

@@ -113,6 +113,13 @@ import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js';
import UndergroundType from './falukant/type/underground.js';
import Underground from './falukant/data/underground.js';
import VehicleType from './falukant/type/vehicle.js';
import Vehicle from './falukant/data/vehicle.js';
import Transport from './falukant/data/transport.js';
import RegionDistance from './falukant/data/region_distance.js';
import WeatherType from './falukant/type/weather.js';
import Weather from './falukant/data/weather.js';
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
import Room from './chat/room.js';
import ChatUser from './chat/user.js';
@@ -207,6 +214,10 @@ const models = {
Credit,
DebtorsPrism,
HealthActivity,
RegionDistance,
VehicleType,
Vehicle,
Transport,
PoliticalOfficeType,
PoliticalOfficeRequirement,
PoliticalOfficeBenefitType,
@@ -220,6 +231,9 @@ const models = {
ElectionHistory,
UndergroundType,
Underground,
WeatherType,
Weather,
ProductWeatherEffect,
Room,
ChatUser,
ChatRight,

View File

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

View File

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

View File

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

View File

@@ -378,10 +378,13 @@ export async function createTriggers() {
tp.election_id,
tp.tp_office_type_id,
tp.tp_election_date,
(
SELECT json_agg(vr)
FROM votes vr
WHERE vr.election_id = tp.election_id
COALESCE(
(
SELECT json_agg(vr)
FROM votes vr
WHERE vr.election_id = tp.election_id
),
'[]'::json -- oder '{}'::json, wenn dir ein Objekt lieber ist
),
NOW() AS created_at,
NOW() AS updated_at

View File

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

View File

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

View File

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

1590
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -18,6 +18,7 @@ router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom);
// --- Users Admin ---
router.get('/users/search', authenticate, adminController.searchUsers);
router.get('/users/statistics', authenticate, adminController.getUserStatistics);
router.get('/users/batch', authenticate, adminController.getUsers);
router.get('/users/:id', authenticate, adminController.getUser);
router.put('/users/:id', authenticate, adminController.updateUser);
@@ -40,6 +41,11 @@ router.get('/falukant/branches/:falukantUserId', authenticate, adminController.g
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
router.get('/falukant/stock-types', authenticate, adminController.getFalukantStockTypes);
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap);
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
// --- Minigames Admin ---
router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns);

View File

@@ -15,6 +15,7 @@ router.get('/branches/types', falukantController.getBranchTypes);
router.get('/branches/:branch', falukantController.getBranch);
router.get('/branches', falukantController.getBranches);
router.post('/branches', falukantController.createBranch);
router.post('/branches/upgrade', falukantController.upgradeBranch);
router.get('/productions', falukantController.getAllProductions);
router.post('/production', falukantController.createProduction);
router.get('/production/:branchId', falukantController.getProduction);
@@ -37,6 +38,7 @@ router.get('/director/:branchId', falukantController.getDirectorForBranch);
router.get('/directors', falukantController.getAllDirectors);
router.post('/directors', falukantController.updateDirector);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.post('/family/set-heir', falukantController.setHeir);
router.get('/family/gifts', falukantController.getGifts);
router.get('/family/children', falukantController.getChildren);
router.post('/family/gift', falukantController.sendGift);
@@ -69,6 +71,17 @@ router.post('/politics/elections', falukantController.vote);
router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections);
router.get('/cities', falukantController.getRegions);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes);
router.post('/vehicles', falukantController.buyVehicles);
router.get('/vehicles', falukantController.getVehicles);
router.post('/vehicles/:vehicleId/repair', falukantController.repairVehicle);
router.post('/vehicles/repair-all', falukantController.repairAllVehicles);
router.post('/transports', falukantController.createTransport);
router.get('/transports/route', falukantController.getTransportRoute);
router.get('/transports/branch/:branchId', falukantController.getBranchTransports);
router.get('/underground/types', falukantController.getUndergroundTypes);
router.get('/notifications', falukantController.getNotifications);
router.get('/notifications/all', falukantController.getAllNotifications);

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

@@ -19,7 +19,9 @@ import Branch from "../models/falukant/data/branch.js";
import FalukantStock from "../models/falukant/data/stock.js";
import FalukantStockType from "../models/falukant/type/stock.js";
import RegionData from "../models/falukant/data/region.js";
import RegionType from "../models/falukant/type/region.js";
import BranchType from "../models/falukant/type/branch.js";
import RegionDistance from "../models/falukant/data/region_distance.js";
import Room from '../models/chat/room.js';
import UserParam from '../models/community/user_param.js';
@@ -298,6 +300,104 @@ class AdminService {
}
}
async getFalukantRegions(userId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const regions = await RegionData.findAll({
attributes: ['id', 'name', 'map'],
include: [
{
model: RegionType,
as: 'regionType',
where: { labelTr: 'city' },
attributes: ['labelTr'],
},
],
order: [['name', 'ASC']],
});
return regions;
}
async updateFalukantRegionMap(userId, regionId, map) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const region = await RegionData.findByPk(regionId);
if (!region) {
throw new Error('regionNotFound');
}
region.map = map || {};
await region.save();
return region;
}
async getRegionDistances(userId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const distances = await RegionDistance.findAll();
return distances;
}
async upsertRegionDistance(userId, { sourceRegionId, targetRegionId, transportMode, distance }) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
if (!sourceRegionId || !targetRegionId || !transportMode) {
throw new Error('missingParameters');
}
const src = await RegionData.findByPk(sourceRegionId);
const tgt = await RegionData.findByPk(targetRegionId);
if (!src || !tgt) {
throw new Error('regionNotFound');
}
const mode = String(transportMode);
const dist = Number(distance);
if (!Number.isFinite(dist) || dist <= 0) {
throw new Error('invalidDistance');
}
const [record] = await RegionDistance.findOrCreate({
where: {
sourceRegionId: src.id,
targetRegionId: tgt.id,
transportMode: mode,
},
defaults: {
distance: dist,
},
});
if (record.distance !== dist) {
record.distance = dist;
await record.save();
}
return record;
}
async deleteRegionDistance(userId, id) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const record = await RegionDistance.findByPk(id);
if (!record) {
throw new Error('notfound');
}
await record.destroy();
return { success: true };
}
async updateFalukantStock(userId, stockId, quantity) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
@@ -441,6 +541,30 @@ class AdminService {
return { id: user.hashedId, username: user.username, active: user.active, registrationDate: user.registrationDate };
}
async getUsersByHashedIds(requestingHashedUserId, targetHashedIds) {
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
throw new Error('noaccess');
}
if (!Array.isArray(targetHashedIds) || targetHashedIds.length === 0) {
return [];
}
const users = await User.findAll({
where: { hashedId: { [Op.in]: targetHashedIds } },
attributes: ['id', 'hashedId', 'username', 'active', 'registrationDate']
});
// Erstelle ein Map für schnellen Zugriff
const userMap = {};
users.forEach(user => {
userMap[user.hashedId] = {
id: user.hashedId,
username: user.username,
active: user.active,
registrationDate: user.registrationDate
};
});
return userMap;
}
async updateUser(requestingHashedUserId, targetHashedId, data) {
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
throw new Error('noaccess');

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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() {
await ProductType.bulkCreate([
// compute min/max cumulative tax across regions
const taxRows = await sequelize.query(`
WITH RECURSIVE ancestors AS (
SELECT id AS start_id, id, parent_id, tax_percent FROM falukant_data.region
UNION ALL
SELECT a.start_id, r.id, r.parent_id, r.tax_percent
FROM falukant_data.region r
JOIN ancestors a ON r.id = a.parent_id
), totals AS (
SELECT start_id, COALESCE(SUM(tax_percent), 0) AS total FROM ancestors GROUP BY start_id
)
SELECT COALESCE(MIN(total),0) AS min_total, COALESCE(MAX(total),0) AS max_total FROM totals
`, { type: sequelize.QueryTypes.SELECT });
const minTax = parseFloat(taxRows?.[0]?.min_total) || 0;
const maxTax = parseFloat(taxRows?.[0]?.max_total) || 0;
const factorMin = (minTax >= 100) ? 1 : (1 / (1 - minTax / 100));
const factorMax = (maxTax >= 100) ? 1 : (1 / (1 - maxTax / 100));
const baseProducts = [
{ labelTr: 'wheat', category: 1, productionTime: 2, sellCost: 7 },
{ labelTr: 'grain', category: 1, productionTime: 2, sellCost: 7 },
{ labelTr: 'carrot', category: 1, productionTime: 1, sellCost: 46},
{ labelTr: 'carrot', category: 1, productionTime: 1, sellCost: 5},
{ labelTr: 'fish', category: 1, productionTime: 2, sellCost: 7 },
{ labelTr: 'meat', category: 1, productionTime: 2, sellCost: 7 },
{ labelTr: 'leather', category: 1, productionTime: 2, sellCost: 7 },
@@ -261,7 +280,11 @@ async function initializeFalukantProducts() {
{ labelTr: 'shield', category: 4, productionTime: 5, sellCost: 60 },
{ labelTr: 'horse', category: 5, productionTime: 5, sellCost: 60 },
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 },
], {
];
const productsToInsert = baseProducts;
await ProductType.bulkCreate(productsToInsert, {
ignoreDuplicates: true,
});
}

View File

@@ -12,11 +12,16 @@ import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js";
import PartyType from "../../models/falukant/type/party.js";
import MusicType from "../../models/falukant/type/music.js";
import BanquetteType from "../../models/falukant/type/banquette.js";
import VehicleType from "../../models/falukant/type/vehicle.js";
import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
import UndergroundType from "../../models/falukant/type/underground.js";
import WeatherType from "../../models/falukant/type/weather.js";
import Weather from "../../models/falukant/data/weather.js";
import ProductWeatherEffect from "../../models/falukant/type/product_weather_effect.js";
import ProductType from "../../models/falukant/type/product.js";
// Debug-Flag: Nur wenn DEBUG_FALUKANT=1 gesetzt ist, werden ausführliche Logs ausgegeben.
const falukantDebug = process.env.DEBUG_FALUKANT === '1';
@@ -41,6 +46,10 @@ export const initializeFalukantTypes = async () => {
await initializePoliticalOfficeTypes();
await initializePoliticalOfficePrerequisites();
await initializeUndergroundTypes();
await initializeVehicleTypes();
await initializeFalukantWeatherTypes();
await initializeFalukantWeathers();
await initializeFalukantProductWeatherEffects();
};
const regionTypes = [];
@@ -273,6 +282,17 @@ const learnerTypes = [
{ tr: 'director', },
];
const vehicleTypes = [
// build times (in minutes): 60, 90, 180, 300, 720, 120, 1440
{ tr: 'cargo_cart', name: 'Lastkarren', cost: 100, capacity: 20, transportMode: 'land', speed: 1, buildTimeMinutes: 60 },
{ tr: 'ox_cart', name: 'Ochsenkarren', cost: 200, capacity: 50, transportMode: 'land', speed: 2, buildTimeMinutes: 90 },
{ tr: 'small_carriage', name: 'kleine Pferdekutsche', cost: 300, capacity: 35, transportMode: 'land', speed: 3, buildTimeMinutes: 180 },
{ tr: 'large_carriage', name: 'große Pferdekutsche', cost: 1000, capacity: 100, transportMode: 'land', speed: 3, buildTimeMinutes: 300 },
{ tr: 'four_horse_carriage', name: 'Vierspänner', cost: 5000, capacity: 200, transportMode: 'land', speed: 4, buildTimeMinutes: 720 },
{ tr: 'raft', name: 'Floß', cost: 100, capacity: 25, transportMode: 'water', speed: 1, buildTimeMinutes: 120 },
{ tr: 'sailing_ship', name: 'Segelschiff', cost: 500, capacity: 200, transportMode: 'water', speed: 3, buildTimeMinutes: 1440 },
];
const politicalOfficeBenefitTypes = [
{ tr: 'salary' },
{ tr: 'reputation' },
@@ -282,6 +302,7 @@ const politicalOfficeBenefitTypes = [
{ tr: 'tax_exemption' },
{ tr: 'guard_protection' },
{ tr: 'court_immunity' },
{ tr: 'set_regionl_tax' },
];
const politicalOffices = [
@@ -883,6 +904,31 @@ export const initializeLearnerTypes = async () => {
}
};
export const initializeVehicleTypes = async () => {
for (const v of vehicleTypes) {
const existing = await VehicleType.findOne({ where: { tr: v.tr } });
if (!existing) {
await VehicleType.create({
tr: v.tr,
cost: v.cost,
capacity: v.capacity,
transportMode: v.transportMode,
speed: v.speed,
buildTimeMinutes: v.buildTimeMinutes,
});
} else {
// ensure new fields like cost/buildTime are updated if missing
await existing.update({
cost: v.cost,
capacity: v.capacity,
transportMode: v.transportMode,
speed: v.speed,
buildTimeMinutes: v.buildTimeMinutes,
});
}
}
};
export const initializePoliticalOfficeBenefitTypes = async () => {
for (const benefitType of politicalOfficeBenefitTypes) {
await PoliticalOfficeBenefitType.findOrCreate({
@@ -972,3 +1018,445 @@ export const initializeFalukantTitlesOfNobility = async () => {
throw error;
}
};
const weatherTypes = [
{ tr: "sunny" },
{ tr: "cloudy" },
{ tr: "rainy" },
{ tr: "stormy" },
{ tr: "snowy" },
{ tr: "foggy" },
{ tr: "windy" },
{ tr: "clear" }
];
export const initializeFalukantWeatherTypes = async () => {
try {
for (const weatherType of weatherTypes) {
await WeatherType.findOrCreate({
where: { tr: weatherType.tr },
});
}
console.log(`[Falukant] Wettertypen initialisiert: ${weatherTypes.length} Typen`);
} catch (error) {
console.error('❌ Fehler beim Initialisieren der Falukant-Wettertypen:', error);
throw error;
}
};
export const initializeFalukantWeathers = async () => {
try {
// Hole alle Städte (Regions vom Typ "city")
const cityRegionType = await RegionType.findOne({ where: { labelTr: 'city' } });
if (!cityRegionType) {
console.warn('[Falukant] Kein RegionType "city" gefunden, überspringe Wetter-Initialisierung');
return;
}
const cities = await RegionData.findAll({
where: { regionTypeId: cityRegionType.id },
attributes: ['id', 'name']
});
// Hole alle Wettertypen
const allWeatherTypes = await WeatherType.findAll();
if (allWeatherTypes.length === 0) {
console.warn('[Falukant] Keine Wettertypen gefunden, überspringe Wetter-Initialisierung');
return;
}
// Weise jeder Stadt zufällig ein Wetter zu
for (const city of cities) {
const randomWeatherType = allWeatherTypes[Math.floor(Math.random() * allWeatherTypes.length)];
await Weather.findOrCreate({
where: { regionId: city.id },
defaults: {
weatherTypeId: randomWeatherType.id
}
});
}
console.log(`[Falukant] Wetter für ${cities.length} Städte initialisiert`);
} catch (error) {
console.error('❌ Fehler beim Initialisieren der Falukant-Wetter:', error);
throw error;
}
};
export const initializeFalukantProductWeatherEffects = async () => {
try {
// Hole alle Produkte und Wettertypen
const products = await ProductType.findAll();
const weatherTypes = await WeatherType.findAll();
if (products.length === 0 || weatherTypes.length === 0) {
console.warn('[Falukant] Keine Produkte oder Wettertypen gefunden, überspringe Produkt-Wetter-Effekte');
return;
}
// Erstelle Map für schnellen Zugriff
const productMap = new Map(products.map(p => [p.labelTr, p.id]));
const weatherMap = new Map(weatherTypes.map(w => [w.tr, w.id]));
// Definiere Effekte für jedes Produkt-Wetter-Paar
// Format: { productLabel: { weatherTr: effectValue } }
// effectValue: -2 (sehr negativ), -1 (negativ), 0 (neutral), 1 (positiv), 2 (sehr positiv)
const effects = {
// Landwirtschaftliche Produkte
wheat: {
sunny: 1, // Gutes Wachstum
cloudy: 0,
rainy: 2, // Wasser ist essentiell
stormy: -1, // Kann Ernte beschädigen
snowy: -2, // Kein Wachstum
foggy: 0,
windy: 0,
clear: 1
},
grain: {
sunny: 1,
cloudy: 0,
rainy: 2,
stormy: -1,
snowy: -2,
foggy: 0,
windy: 0,
clear: 1
},
carrot: {
sunny: 1,
cloudy: 0,
rainy: 2,
stormy: -1,
snowy: -2,
foggy: 0,
windy: 0,
clear: 1
},
fish: {
sunny: 0,
cloudy: 0,
rainy: 0,
stormy: -2, // Gefährlich zu fischen
snowy: -1, // Kaltes Wasser
foggy: -1, // Schlechte Sicht
windy: -1, // Schwierig zu fischen
clear: 1
},
meat: {
sunny: -1, // Kann verderben
cloudy: 0,
rainy: -1, // Feucht
stormy: -2,
snowy: 1, // Kühlt
foggy: 0,
windy: 0,
clear: 0
},
leather: {
sunny: -1, // Kann austrocknen
cloudy: 0,
rainy: -1, // Feucht
stormy: -2,
snowy: 1, // Kühlt
foggy: 0,
windy: 0,
clear: 0
},
wood: {
sunny: 1, // Trocknet gut
cloudy: 0,
rainy: -1, // Feucht
stormy: -2, // Kann beschädigt werden
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 1
},
stone: {
sunny: 0,
cloudy: 0,
rainy: 0,
stormy: 0,
snowy: 0,
foggy: 0,
windy: 0,
clear: 0
},
milk: {
sunny: -1, // Kann sauer werden
cloudy: 0,
rainy: 0,
stormy: -1,
snowy: 1, // Kühlt
foggy: 0,
windy: 0,
clear: 0
},
cheese: {
sunny: -1,
cloudy: 0,
rainy: -1, // Feucht
stormy: -1,
snowy: 1, // Kühlt
foggy: 0,
windy: 0,
clear: 0
},
bread: {
sunny: 0,
cloudy: 0,
rainy: -1, // Feucht
stormy: -1,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
beer: {
sunny: 0,
cloudy: 0,
rainy: 0,
stormy: 0,
snowy: 1, // Kühlt
foggy: 0,
windy: 0,
clear: 0
},
iron: {
sunny: 0,
cloudy: 0,
rainy: -1, // Rost
stormy: -2, // Rost
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
copper: {
sunny: 0,
cloudy: 0,
rainy: -1, // Oxidation
stormy: -2,
snowy: 0,
foggy: -1,
windy: 0,
clear: 0
},
spices: {
sunny: 0,
cloudy: 0,
rainy: -1, // Feucht
stormy: -1,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
salt: {
sunny: 1, // Trocknet gut
cloudy: 0,
rainy: -2, // Löst sich auf
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 1
},
sugar: {
sunny: 0,
cloudy: 0,
rainy: -2, // Löst sich auf
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
vinegar: {
sunny: 1, // Heißes Wetter fördert Gärung
cloudy: 0,
rainy: 0,
stormy: 0,
snowy: -1, // Kaltes Wetter hemmt Gärung
foggy: 0,
windy: 0,
clear: 1 // Heißes Wetter fördert Gärung
},
cotton: {
sunny: 1, // Trocknet gut
cloudy: 0,
rainy: -1, // Feucht
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 1
},
wine: {
sunny: 0,
cloudy: 0,
rainy: 0,
stormy: 0,
snowy: 1, // Kühlt
foggy: 0,
windy: 0,
clear: 0
},
gold: {
sunny: 0,
cloudy: 0,
rainy: 0,
stormy: 0,
snowy: 0,
foggy: 0,
windy: 0,
clear: 0
},
diamond: {
sunny: 0,
cloudy: 0,
rainy: 0,
stormy: 0,
snowy: 0,
foggy: 0,
windy: 0,
clear: 0
},
furniture: {
sunny: 0,
cloudy: 0,
rainy: -1, // Feucht
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
clothing: {
sunny: 0,
cloudy: 0,
rainy: -1, // Feucht
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
jewelry: {
sunny: 0,
cloudy: 0,
rainy: -2, // Kann beschädigt werden
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
painting: {
sunny: -1, // Kann verblassen
cloudy: 0,
rainy: -2, // Feucht
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: -1
},
book: {
sunny: -1, // Kann verblassen
cloudy: 0,
rainy: -2, // Feucht
stormy: -2,
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: -1
},
weapon: {
sunny: 0,
cloudy: 0,
rainy: -1, // Rost
stormy: -2, // Rost
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
armor: {
sunny: 0,
cloudy: 0,
rainy: -1, // Rost
stormy: -2, // Rost
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
shield: {
sunny: 0,
cloudy: 0,
rainy: -1, // Rost
stormy: -2, // Rost
snowy: 0,
foggy: -1, // Feucht
windy: 0,
clear: 0
},
horse: {
sunny: 1, // Gutes Wetter
cloudy: 0,
rainy: -1, // Nass
stormy: -2, // Angst
snowy: -1, // Kalt
foggy: 0,
windy: 0,
clear: 1
},
ox: {
sunny: 1, // Gutes Wetter
cloudy: 0,
rainy: -1, // Nass
stormy: -2, // Angst
snowy: -1, // Kalt
foggy: 0,
windy: 0,
clear: 1
}
};
// Erstelle alle Produkt-Wetter-Effekte
const effectEntries = [];
for (const [productLabel, weatherEffects] of Object.entries(effects)) {
const productId = productMap.get(productLabel);
if (!productId) {
console.warn(`[Falukant] Produkt "${productLabel}" nicht gefunden, überspringe`);
continue;
}
for (const [weatherTr, effectValue] of Object.entries(weatherEffects)) {
const weatherTypeId = weatherMap.get(weatherTr);
if (!weatherTypeId) {
console.warn(`[Falukant] Wettertyp "${weatherTr}" nicht gefunden, überspringe`);
continue;
}
effectEntries.push({
productId,
weatherTypeId,
qualityEffect: effectValue
});
}
}
// Bulk insert mit ignoreDuplicates
await ProductWeatherEffect.bulkCreate(effectEntries, {
ignoreDuplicates: true
});
console.log(`[Falukant] Produkt-Wetter-Effekte initialisiert: ${effectEntries.length} Einträge`);
} catch (error) {
console.error('❌ Fehler beim Initialisieren der Produkt-Wetter-Effekte:', error);
throw error;
}
};

View File

@@ -45,6 +45,16 @@ const createSchemas = async () => {
const initializeDatabase = async () => {
await createSchemas();
// Aktiviere die pgcrypto Erweiterung für die digest() Funktion
try {
await sequelize.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;');
console.log('✅ pgcrypto Erweiterung aktiviert');
} catch (error) {
console.warn('⚠️ Konnte pgcrypto Erweiterung nicht aktivieren:', error.message);
// Fortfahren, da die Erweiterung möglicherweise bereits aktiviert ist
}
// Modelle nur laden, aber an dieser Stelle NICHT syncen.
// Das Syncing (inkl. alter: true bei Bedarf) wird anschließend zentral
// über syncModelsWithUpdates()/syncModelsAlways gesteuert.
@@ -95,7 +105,8 @@ const syncModelsWithUpdates = async (models) => {
if (needsUpdate) {
console.log('🔄 Schema-Updates nötig - verwende alter: true');
for (const model of Object.values(models)) {
await model.sync({ alter: true, force: false });
// constraints: false verhindert, dass Sequelize Foreign Keys automatisch erstellt
await model.sync({ alter: true, force: false, constraints: false });
}
console.log('✅ Schema-Updates abgeschlossen');
} else {
@@ -363,7 +374,8 @@ const getExpectedDefaultValue = (defaultValue) => {
const updateSchema = async (models) => {
console.log('🔄 Aktualisiere Datenbankschema...');
for (const model of Object.values(models)) {
await model.sync({ alter: true, force: false });
// constraints: false verhindert, dass Sequelize Foreign Keys automatisch erstellt
await model.sync({ alter: true, force: false, constraints: false });
}
console.log('✅ Datenbankschema aktualisiert');
};
@@ -407,7 +419,310 @@ const syncModelsAlways = async (models) => {
try {
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');
} catch (error) {

View File

@@ -54,6 +54,112 @@ const syncDatabase = async () => {
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e);
}
// Cleanup: Entferne verwaiste Einträge vor Schema-Updates (nur wenn Schema-Updates aktiviert)
if (currentStage === 'dev') {
console.log("Cleaning up orphaned entries...");
try {
// Cleanup user_param_visibility
const result1 = await sequelize.query(`
DELETE FROM community.user_param_visibility
WHERE param_id NOT IN (
SELECT id FROM community.user_param
);
`);
const deletedCount1 = result1[1] || 0;
if (deletedCount1 > 0) {
console.log(`${deletedCount1} verwaiste user_param_visibility Einträge entfernt`);
}
// Cleanup stock mit ungültigen branch_id (0 oder nicht existierend)
const result2 = await sequelize.query(`
DELETE FROM falukant_data.stock
WHERE branch_id = 0 OR branch_id NOT IN (
SELECT id FROM falukant_data.branch
);
`);
const deletedCount2 = result2[1] || 0;
if (deletedCount2 > 0) {
console.log(`${deletedCount2} verwaiste stock Einträge entfernt`);
}
// Cleanup knowledge mit ungültigen character_id oder product_id
const result3 = await sequelize.query(`
DELETE FROM falukant_data.knowledge
WHERE character_id NOT IN (
SELECT id FROM falukant_data.character
) OR product_id NOT IN (
SELECT id FROM falukant_type.product
);
`);
const deletedCount3 = result3[1] || 0;
if (deletedCount3 > 0) {
console.log(`${deletedCount3} verwaiste knowledge Einträge entfernt`);
}
// Cleanup notification mit ungültigen user_id
const result4 = await sequelize.query(`
DELETE FROM falukant_log.notification
WHERE user_id NOT IN (
SELECT id FROM falukant_data.falukant_user
);
`);
const deletedCount4 = result4[1] || 0;
if (deletedCount4 > 0) {
console.log(`${deletedCount4} verwaiste notification Einträge entfernt`);
}
// Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id
const result5 = await sequelize.query(`
DELETE FROM falukant_log.promotional_gift
WHERE sender_character_id NOT IN (
SELECT id FROM falukant_data.character
) OR recipient_character_id NOT IN (
SELECT id FROM falukant_data.character
);
`);
const deletedCount5 = result5[1] || 0;
if (deletedCount5 > 0) {
console.log(`${deletedCount5} verwaiste promotional_gift Einträge entfernt`);
}
// Cleanup user_house mit ungültigen house_type_id oder user_id
const result6 = await sequelize.query(`
DELETE FROM falukant_data.user_house
WHERE house_type_id NOT IN (
SELECT id FROM falukant_type.house
) OR user_id NOT IN (
SELECT id FROM falukant_data.falukant_user
);
`);
const deletedCount6 = result6[1] || 0;
if (deletedCount6 > 0) {
console.log(`${deletedCount6} verwaiste user_house Einträge entfernt`);
}
// Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id
const result7 = await sequelize.query(`
DELETE FROM falukant_data.child_relation
WHERE father_character_id NOT IN (
SELECT id FROM falukant_data.character
) OR mother_character_id NOT IN (
SELECT id FROM falukant_data.character
) OR child_character_id NOT IN (
SELECT id FROM falukant_data.character
);
`);
const deletedCount7 = result7[1] || 0;
if (deletedCount7 > 0) {
console.log(`${deletedCount7} verwaiste child_relation Einträge entfernt`);
}
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0) {
console.log("✅ Keine verwaisten Einträge gefunden");
}
} catch (e) {
console.warn('⚠️ Konnte verwaiste Einträge nicht bereinigen:', e?.message || e);
}
}
console.log("Setting up associations...");
setupAssociations();
@@ -104,6 +210,10 @@ const syncDatabase = async () => {
// Deployment-Synchronisation (immer Schema-Updates)
const syncDatabaseForDeployment = async () => {
try {
// WICHTIG: Bei Caching-Problemen das Script neu starten
// Node.js cached ES-Module, daher müssen Models neu geladen werden
console.log('📦 Lade Models neu (Node.js Module-Cache wird verwendet)...');
// Zeige den aktuellen Stage an
const currentStage = process.env.STAGE || 'nicht gesetzt';
console.log(`🚀 Starte Datenbank-Synchronisation für Deployment (Stage: ${currentStage})`);
@@ -133,6 +243,165 @@ const syncDatabaseForDeployment = async () => {
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e);
}
// Migration: Transport product_id und size nullable machen
console.log("Making transport product_id and size nullable...");
try {
await sequelize.query(`
DO $$
BEGIN
-- Prüfe ob product_id NOT NULL Constraint existiert
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'transport'
AND column_name = 'product_id'
AND is_nullable = 'NO'
) THEN
ALTER TABLE falukant_data.transport
ALTER COLUMN product_id DROP NOT NULL;
RAISE NOTICE 'product_id NOT NULL Constraint entfernt';
END IF;
-- Prüfe ob size NOT NULL Constraint existiert
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'transport'
AND column_name = 'size'
AND is_nullable = 'NO'
) THEN
ALTER TABLE falukant_data.transport
ALTER COLUMN size DROP NOT NULL;
RAISE NOTICE 'size NOT NULL Constraint entfernt';
END IF;
END
$$;
`);
console.log("✅ Transport product_id und size sind jetzt nullable");
} catch (e) {
console.warn('⚠️ Konnte Transport-Spalten nicht nullable machen:', e?.message || e);
}
// Cleanup: Entferne verwaiste Einträge vor Schema-Updates
console.log("Cleaning up orphaned entries...");
try {
// Cleanup user_param_visibility
const result1 = await sequelize.query(`
DELETE FROM community.user_param_visibility
WHERE param_id NOT IN (
SELECT id FROM community.user_param
);
`);
const deletedCount1 = result1[1] || 0;
if (deletedCount1 > 0) {
console.log(`${deletedCount1} verwaiste user_param_visibility Einträge entfernt`);
}
// Cleanup stock mit ungültigen branch_id (0 oder nicht existierend)
const result2 = await sequelize.query(`
DELETE FROM falukant_data.stock
WHERE branch_id = 0 OR branch_id NOT IN (
SELECT id FROM falukant_data.branch
);
`);
const deletedCount2 = result2[1] || 0;
if (deletedCount2 > 0) {
console.log(`${deletedCount2} verwaiste stock Einträge entfernt`);
}
// Cleanup knowledge mit ungültigen character_id oder product_id
const result3 = await sequelize.query(`
DELETE FROM falukant_data.knowledge
WHERE character_id NOT IN (
SELECT id FROM falukant_data.character
) OR product_id NOT IN (
SELECT id FROM falukant_type.product
);
`);
const deletedCount3 = result3[1] || 0;
if (deletedCount3 > 0) {
console.log(`${deletedCount3} verwaiste knowledge Einträge entfernt`);
}
// Cleanup notification mit ungültigen user_id
const result4 = await sequelize.query(`
DELETE FROM falukant_log.notification
WHERE user_id NOT IN (
SELECT id FROM falukant_data.falukant_user
);
`);
const deletedCount4 = result4[1] || 0;
if (deletedCount4 > 0) {
console.log(`${deletedCount4} verwaiste notification Einträge entfernt`);
}
// Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id
const result5 = await sequelize.query(`
DELETE FROM falukant_log.promotional_gift
WHERE sender_character_id NOT IN (
SELECT id FROM falukant_data.character
) OR recipient_character_id NOT IN (
SELECT id FROM falukant_data.character
);
`);
const deletedCount5 = result5[1] || 0;
if (deletedCount5 > 0) {
console.log(`${deletedCount5} verwaiste promotional_gift Einträge entfernt`);
}
// Cleanup user_house mit ungültigen house_type_id oder user_id
const result6 = await sequelize.query(`
DELETE FROM falukant_data.user_house
WHERE house_type_id NOT IN (
SELECT id FROM falukant_type.house
) OR user_id NOT IN (
SELECT id FROM falukant_data.falukant_user
);
`);
const deletedCount6 = result6[1] || 0;
if (deletedCount6 > 0) {
console.log(`${deletedCount6} verwaiste user_house Einträge entfernt`);
}
// Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id
const result7 = await sequelize.query(`
DELETE FROM falukant_data.child_relation
WHERE father_character_id NOT IN (
SELECT id FROM falukant_data.character
) OR mother_character_id NOT IN (
SELECT id FROM falukant_data.character
) OR child_character_id NOT IN (
SELECT id FROM falukant_data.character
);
`);
const deletedCount7 = result7[1] || 0;
if (deletedCount7 > 0) {
console.log(`${deletedCount7} verwaiste child_relation Einträge entfernt`);
}
// Cleanup political_office mit ungültigen character_id, office_type_id oder region_id
const result8 = await sequelize.query(`
DELETE FROM falukant_data.political_office
WHERE character_id NOT IN (
SELECT id FROM falukant_data.character
) OR office_type_id NOT IN (
SELECT id FROM falukant_type.political_office_type
) OR region_id NOT IN (
SELECT id FROM falukant_data.region
);
`);
const deletedCount8 = result8[1] || 0;
if (deletedCount8 > 0) {
console.log(`${deletedCount8} verwaiste political_office Einträge entfernt`);
}
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0) {
console.log("✅ Keine verwaisten Einträge gefunden");
}
} catch (e) {
console.warn('⚠️ Konnte verwaiste Einträge nicht bereinigen:', e?.message || e);
}
console.log("Setting up associations...");
setupAssociations();

1
deploy Symbolic link
View File

@@ -0,0 +1 @@
/opt/yourpart

View File

@@ -26,6 +26,7 @@ fi
# 5. Frontend neu bauen VITE_* aus Environment übernehmen oder Defaults setzen
echo "Baue Frontend neu..."
export VITE_API_BASE_URL=${VITE_API_BASE_URL:-https://www.your-part.de}
# Standard: Daemon direkt auf Port 4551, nicht über Apache-Proxy
export VITE_DAEMON_SOCKET=${VITE_DAEMON_SOCKET:-wss://www.your-part.de:4551}
export VITE_CHAT_WS_URL=${VITE_CHAT_WS_URL:-wss://www.your-part.de:1235}

142
deploy-with-config.sh Executable file
View File

@@ -0,0 +1,142 @@
#!/bin/bash
echo "=== YourPart Deployment mit vorhandener Konfiguration ==="
# Prüfen ob wir im richtigen Verzeichnis sind
if [ ! -f "package.json" ]; then
echo "Error: Bitte führen Sie dieses Skript aus dem YourPart3-Root-Verzeichnis aus"
exit 1
fi
# Frontend bauen
echo "Building frontend..."
cd frontend
npm ci
npm run build
cd ..
# Verzeichnisse erstellen
echo "Creating directories..."
sudo mkdir -p /opt/yourpart/{frontend,backend,images/{tmp,userimages,screenshots}}
# Frontend kopieren
echo "Deploying frontend..."
sudo cp -r frontend/dist /opt/yourpart/frontend/
sudo chown -R www-data:www-data /opt/yourpart/frontend
sudo chmod -R 755 /opt/yourpart/frontend
# Backend kopieren
echo "Deploying backend..."
sudo cp -r backend/* /opt/yourpart/backend/
sudo chown -R www-data:www-data /opt/yourpart/backend
sudo chmod -R 755 /opt/yourpart/backend
# .env-Datei erstellen
echo "Creating .env file..."
cat > /opt/yourpart/backend/.env << 'ENVEOF'
# Datenbank-Konfiguration (aus alter App)
DB_HOST=localhost
DB_USER=yourpart
DB_PASS=hitomisan
DB_NAME=yp2
DB_PORT=60000
# Anwendungskonfiguration
NODE_ENV=production
PORT=2020
STAGE=production
# Redis-Konfiguration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASS=your_redis_password
# Session-Konfiguration (aus alter App)
SECRET_KEY=k7e0CCw75PcmEGa
SESSION_SECRET=k7e0CCw75PcmEGa
# E-Mail-Konfiguration (aus alter App)
SMTP_HOST=smtp.1blu.de
SMTP_PORT=465
SMTP_USER=e226079_0-tsschulz
SMTP_PASS=hitomisan
SMTP_SECURE=true
# E-Mail-Einstellungen (aus alter App)
SENDER_NAME=YourPart
SENDER_EMAIL=kontakt@your-part.de
# AMQP-Konfiguration (aus alter App)
AMQP_HOST=tsschulz.de
AMQP_PORT=5672
AMQP_EXCHANGE=yourpart
AMQP_USERNAME=yourpart
AMQP_PASSWORD=yourpart
# API-Keys (aus alter App)
WEATHER_API_KEY=d0ddfcbc915f50263274211648a5dab0
NEWS_API_KEY=pub_212733602779de7708a7374d67e363bd06af4
# Pfad-Konfiguration (aus alter App)
ROOT_PATH=/opt/yourpart
IMAGES_PATH=/images
TMP_IMAGES_PATH=/images/tmp
USER_IMAGES_PATH=/images/userimages
SCREENSHOT_IMAGES_PATH=/images/screenshots
# URL-Konfiguration
BASE_URL=https://www.your-part.de
IMAGES_URL=https://www.your-part.de/images/
ACTIVATION_URL=https://www.your-part.de/activate?code=
PASSWORD_RESET_URL=https://www.your-part.de/setnewpassword?username=
# Spiel-Konfiguration (aus alter App)
START_BUDGET=10
TEST_MODE=false
PAY_PER_DAY=100
# Debug-Einstellungen
DEBUG_SQL=false
DEBUG_MESSAGES=false
DEBUG_RECREATE_DB=false
ENVEOF
sudo chown www-data:www-data /opt/yourpart/backend/.env
sudo chmod 600 /opt/yourpart/backend/.env
# Service installieren
echo "Installing service..."
sudo cp yourpart.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable yourpart.service
# Apache-Konfiguration
echo "Configuring Apache..."
sudo cp yourpart-http.conf /etc/apache2/sites-available/
sudo cp yourpart-https.conf /etc/apache2/sites-available/
# Alte Konfiguration deaktivieren
sudo a2dissite yourpart 2>/dev/null || true
# Neue Konfigurationen aktivieren
sudo a2ensite yourpart-http
sudo a2ensite yourpart-https
# Apache-Module aktivieren
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite ssl
sudo systemctl reload apache2
# Service starten
echo "Starting service..."
sudo systemctl start yourpart.service
echo ""
echo "=== Deployment abgeschlossen! ==="
echo "Frontend: /opt/yourpart/frontend/dist/"
echo "Backend: /opt/yourpart/backend/"
echo "Service: yourpart.service"
echo ""
echo "Status prüfen:"
echo " sudo systemctl status yourpart.service"
echo " sudo systemctl status apache2"
echo " sudo journalctl -u yourpart.service -f"

26
fix-api-urls.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
echo "=== YourPart API-URL Fix ==="
cd ~/yourpart3/frontend
# 1. Alle localhost:3001 Referenzen finden
echo "Suche nach localhost:3001 Referenzen..."
grep -r "localhost:3001" src/ || echo "Keine localhost:3001 Referenzen gefunden"
# 2. Alle localhost Referenzen finden
echo ""
echo "Suche nach localhost Referenzen..."
grep -r "localhost" src/ || echo "Keine localhost Referenzen gefunden"
# 3. API-Konfigurationsdateien finden
echo ""
echo "Suche nach API-Konfigurationsdateien..."
find src/ -name "*.js" -exec grep -l "axios\|baseURL\|localhost" {} \;
echo ""
echo "=== API-URL Fix abgeschlossen ==="
echo "Bitte überprüfen Sie die gefundenen Dateien und ersetzen Sie:"
echo " localhost:3001 → /api"
echo " http://localhost:3001 → /api"
echo " baseURL: 'http://localhost:3001' → baseURL: '/api'"

22
fix-cors.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
echo "=== YourPart CORS-Fix ==="
# Backup der ursprünglichen app.js erstellen
sudo cp /opt/yourpart/backend/app.js /opt/yourpart/backend/app.js.backup
# CORS-Konfiguration aktualisieren
sudo sed -i 's|origin: \[.*\]|origin: [\n "http://localhost:3000", \n "http://localhost:5173", \n "http://127.0.0.1:3000", \n "http://127.0.0.1:5173",\n "https://your-part.de",\n "https://www.your-part.de",\n "http://your-part.de",\n "http://www.your-part.de"\n ]|' /opt/yourpart/backend/app.js
echo "CORS-Konfiguration aktualisiert!"
# Service neu starten
echo "Starte Backend-Service neu..."
sudo systemctl restart yourpart.service
# Status prüfen
echo "Service-Status:"
sudo systemctl status yourpart.service
echo ""
echo "CORS-Fix abgeschlossen! Testen Sie jetzt die Anwendung."

5
frontend/.env.production Normal file
View File

@@ -0,0 +1,5 @@
VITE_API_BASE_URL=https://www.your-part.de
VITE_TINYMCE_API_KEY=xjqnfymt2wd5q95onkkwgblzexams6l6naqjs01x72ftzryg
VITE_DAEMON_SOCKET=wss://www.your-part.de:4551
VITE_CHAT_WS_URL=wss://www.your-part.de:1235
VITE_SOCKET_IO_URL=https://www.your-part.de:4443

View File

@@ -530,13 +530,13 @@
}
},
"node_modules/@intlify/core-base": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.7.tgz",
"integrity": "sha512-mE71aUH5baH0me8duB4FY5qevUJizypHsYw3eCvmOx07QvmKppgOONx3dYINxuA89Z2qkAGb/K6Nrpi7aAMwew==",
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.8.tgz",
"integrity": "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "10.0.7",
"@intlify/shared": "10.0.7"
"@intlify/message-compiler": "10.0.8",
"@intlify/shared": "10.0.8"
},
"engines": {
"node": ">= 16"
@@ -546,12 +546,12 @@
}
},
"node_modules/@intlify/message-compiler": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.7.tgz",
"integrity": "sha512-nrC4cDL/UHZSUqd8sRbVz+DPukzZ8NnG5OK+EB/nlxsH35deyzyVkXP/QuR8mFZrISJ+4hCd6VtCQCcT+RO+5g==",
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.8.tgz",
"integrity": "sha512-DV+sYXIkHVd5yVb2mL7br/NEUwzUoLBsMkV3H0InefWgmYa34NLZUvMCGi5oWX+Hqr2Y2qUxnVrnOWF4aBlgWg==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "10.0.7",
"@intlify/shared": "10.0.8",
"source-map-js": "^1.0.2"
},
"engines": {
@@ -562,9 +562,9 @@
}
},
"node_modules/@intlify/shared": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.7.tgz",
"integrity": "sha512-oeoq0L5+5P4ShXa6jBQcx+BT+USe3MjX0xJexZO1y7rfDJdwZ9+QP3jO4tcS1nxhBYYdjvFTqe4bmnLijV0GxQ==",
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.8.tgz",
"integrity": "sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==",
"license": "MIT",
"engines": {
"node": ">= 16"
@@ -1494,7 +1494,8 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
@@ -1512,13 +1513,13 @@
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -1565,6 +1566,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -1593,6 +1607,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -1674,6 +1689,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
@@ -1698,6 +1714,20 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/engine.io-client": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
@@ -1730,13 +1760,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -1745,7 +1772,33 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
@@ -1848,12 +1901,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -1878,22 +1934,26 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1902,6 +1962,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -1915,12 +1988,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.1.3"
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1938,23 +2011,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -1966,7 +2027,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -1981,7 +2041,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -2151,6 +2210,15 @@
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
@@ -2161,6 +2229,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -2169,6 +2238,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
@@ -2864,9 +2934,9 @@
"dev": true
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2987,13 +3057,13 @@
}
},
"node_modules/vue-i18n": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.7.tgz",
"integrity": "sha512-bKsk0PYwP9gdYF4nqSAT0kDpnLu1gZzlxFl885VH4mHVhEnqP16+/mAU05r1U6NIrc0fGDWP89tZ8GzeJZpe+w==",
"version": "10.0.8",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.8.tgz",
"integrity": "sha512-mIjy4utxMz9lMMo6G9vYePv7gUFt4ztOMhY9/4czDJxZ26xPeJ49MAGa9wBAE3XuXbYCrtVPmPxNjej7JJJkZQ==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "10.0.7",
"@intlify/shared": "10.0.7",
"@intlify/core-base": "10.0.8",
"@intlify/shared": "10.0.8",
"@vue/devtools-api": "^6.5.0"
},
"engines": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.6 KiB

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