Compare commits

..

269 Commits

Author SHA1 Message Date
Torsten Schulz (local)
2958d38c63 Update z-index values in DialogWidget and OverviewView components to improve layout stacking context and ensure proper element visibility. 2026-02-05 10:46:16 +01:00
Torsten Schulz (local)
aecd9a8245 Update z-index values in DialogWidget and StatusBar components, and remove negative z-index in OverviewView component for improved layout and stacking context. 2026-02-05 08:41:15 +01:00
Torsten Schulz (local)
4f3439e835 Update syncDatabase function to remove setupAssociations call: Added comments to clarify that associations should be set by the caller to prevent AssociationError during synchronization. 2026-02-05 08:25:38 +01:00
Torsten Schulz (local)
a5bec5baf7 Update CSS styles in OverviewView component: Added relative positioning and z-index to enhance layout control and ensure proper stacking of elements. 2026-02-05 08:19:29 +01:00
Torsten Schulz (local)
8d23453371 Add watcher for branchId in DirectorInfo component: Implemented a watcher to trigger loadDirector method when branchId changes, enhancing data responsiveness in the UI. 2026-02-04 15:44:16 +01:00
Torsten Schulz (local)
2184c4a7e1 Import ProductPriceHistory model in associations.js to establish necessary relationships for product pricing data management. 2026-02-04 15:37:41 +01:00
Torsten Schulz (local)
ba5e36fa55 Initialize model associations in server setup: Import and invoke setupAssociations to ensure all models share the same instance, preventing EagerLoadingError and enhancing data integrity before loading the app and services. 2026-02-04 15:27:46 +01:00
Torsten Schulz (local)
70d1d48fbc Refactor vehicle loading in FalukantService: Update logic to load vehicle types and transports using separate queries, improving performance and preventing EagerLoadingError. Simplify data retrieval by mapping vehicle types and organizing transports by vehicle ID, enhancing code clarity and efficiency. 2026-02-04 15:20:40 +01:00
Torsten Schulz (local)
d23026121e Refactor child character loading in FalukantService: Update logic to load child relations using separate queries, improving performance and preventing EagerLoadingError. Simplify data retrieval by eliminating unnecessary eager loading and enhancing clarity in the code structure. 2026-02-04 15:16:05 +01:00
Torsten Schulz (local)
057b038fac Refactor relationship loading in FalukantService: Update logic to load relationships without eager loading, preventing EagerLoadingError. Enhance data retrieval by using separate queries for traits and moods, improving performance and reliability. 2026-02-04 15:12:36 +01:00
Torsten Schulz (local)
0697f3d363 Refactor trait and mood field names in FalukantService: Update database query conditions to use camelCase for traitId and moodId, ensuring consistency with model definitions and improving code readability. 2026-02-04 15:07:05 +01:00
Torsten Schulz (local)
400d44289c Update primary key constraints in promotional gift models: Set giftId and traitId as primary keys in PromotionalGiftCharacterTrait and PromotionalGiftMood models to ensure data integrity. Enhance FalukantService to load character traits and user house data with separate queries, improving performance and preventing EagerLoadingError. 2026-02-04 15:05:19 +01:00
Torsten Schulz (local)
bbc3354f16 Refactor user house retrieval in FalukantService: Update logic to check for user ID before querying UserHouse, preventing unnecessary database calls and improving error handling. Maintain separate queries for UserHouse and relationships to avoid EagerLoadingError. 2026-02-04 14:20:22 +01:00
Torsten Schulz (local)
d038d72cde Refactor user and relationship data retrieval in FalukantService: Update loading logic to use separate queries for UserHouse and relationships, improving reliability and preventing EagerLoadingError. Enhance heir selection UI with new translations in German and English, providing better user guidance during character selection. 2026-02-04 14:12:25 +01:00
Torsten Schulz (local)
16e54d20d0 Refactor user and relationship loading in FalukantService: Replace eager loading of UserHouse and relationships with separate queries to prevent EagerLoadingError. This change enhances data retrieval reliability and maintains performance during user-related operations. 2026-02-04 14:01:28 +01:00
Torsten Schulz (local)
14775eb556 Add product price history model and database schema: Implement associations for ProductPriceHistory with ProductType and RegionData. Update FalukantService to utilize active character for user-related operations. Ensure product price history table exists in the database with appropriate structure and indexing. 2026-02-04 09:02:51 +01:00
Torsten Schulz (local)
ce34bae16a Enhance Bisaya course content creation: Update SQL query to include owner user ID and improve exercise handling logic. Implement checks for existing exercises and placeholder replacements for review lessons, ensuring better management of lesson content and user feedback. 2026-02-02 09:58:35 +01:00
Torsten Schulz (local)
640cdcf671 Add new exercises and vocabulary tests for Bisaya language course: Introduce multiple-choice questions for Week 1, covering greetings, family terms, and common phrases. Enhance learning materials with explanations for each question to aid understanding. 2026-02-02 09:47:59 +01:00
Torsten Schulz (local)
f15924c0be Adjust character positioning in OverviewView: Change the bottom offset of the character foreground from 10px to -15px to improve visual alignment within the user interface. 2026-02-02 09:35:16 +01:00
Torsten Schulz (local)
0d32c5b4b3 Update character styling in OverviewView: Adjust character positioning and dimensions by changing the bottom offset to 10px and increasing width and height to 55%. This enhances the visual representation of characters in the user interface. 2026-02-02 09:03:33 +01:00
Torsten Schulz (local)
101050ce58 Add avatar display logic in OverviewView: Introduce dynamic avatar styling based on user gender and age, enhancing character representation. Implement age group categorization for precise avatar positioning and dimensions, improving visual consistency in the user interface. 2026-02-02 08:53:47 +01:00
Torsten Schulz (local)
b16249e7c2 Implement noBackground prop in Character3D component: Add functionality to conditionally render the background and background image based on the noBackground prop. Update OverviewView to utilize the new prop, enhancing the character display logic and improving visual customization options. 2026-02-02 08:46:12 +01:00
Torsten Schulz (local)
8b63344bc2 Enhance proposal generation logic in FalukantService: Refactor character selection process to include region and age filters, improving the randomness and relevance of director character proposals. Implement fallback mechanism for character retrieval to ensure availability, enhancing overall proposal generation reliability. 2026-02-02 08:24:28 +01:00
Torsten Schulz (local)
b648175205 Refactor trigger creation logic: Update the created_at and updated_at fields to use the election date instead of the current timestamp, ensuring accurate term end calculations. Enhance socket event handling in StatusBar component by implementing setup and teardown methods for improved event management and user notifications. 2026-02-02 07:48:53 +01:00
Torsten Schulz (local)
4bf1bc35ae Enhance supervisor office validation in falukantService: Update error handling to provide clearer messages when no supervisor position exists or when the position is vacant. This change improves user feedback during application processes by ensuring users understand the requirements for supervisor office fulfillment. 2026-01-31 00:23:20 +01:00
Torsten Schulz (local)
067273d428 Refactor event handling in CalendarView: Simplify event update and creation logic by removing unnecessary response handling. Implement event reloading after save and delete operations to ensure a clean state. This enhancement improves the reliability of event management in the calendar. 2026-01-30 15:43:28 +01:00
Torsten Schulz (local)
7ed284d74b Add widget functionality for birthdays, upcoming events, and mini calendar: Implement new API endpoints in calendarController and calendarService to retrieve upcoming birthdays and events, as well as mini calendar data. Update calendarRouter to include widget routes and enhance DashboardWidget to dynamically render new widget components. This update improves user experience by providing quick access to important calendar information. 2026-01-30 15:14:37 +01:00
Torsten Schulz (local)
f65d3385ec Add friends' birthdays feature: Implement API endpoint to retrieve friends' birthdays for a specified year, enhance calendar service to handle visibility checks, and update CalendarView to display birthday events with distinct styling. This update improves user experience by allowing users to view important dates of their friends. 2026-01-30 14:59:32 +01:00
Torsten Schulz (local)
7635355e94 Add all-day event functionality to CalendarView: Introduce a new section for displaying all-day events in both week and day views. Update localization files to include translations for 'All Day' in English and German. Enhance event handling methods to support all-day events, improving the overall calendar experience. 2026-01-30 14:43:07 +01:00
Torsten Schulz (local)
ec75c7ecdb Improve date handling in CalendarView: Add comments to clarify the logic for setting the date to the first of the month to prevent overflow issues when navigating between months. This enhancement aids in maintaining accurate date transitions in the calendar view. 2026-01-30 14:41:02 +01:00
Torsten Schulz (local)
786420d1d2 Refactor date handling in CalendarView: Update date formatting to use local timezone instead of UTC, enhancing accuracy in date representation. Introduce a new helper method for consistent date formatting across the component. This change improves the overall user experience by ensuring dates are displayed correctly. 2026-01-30 14:35:08 +01:00
Torsten Schulz (local)
cff0ce1e1a Add calendar feature: Integrate calendarRouter and CalendarEvent model, enhance CalendarView with API interactions for event management, and update localization files for error handling in both English and German. This update improves the calendar functionality and user experience. 2026-01-30 14:29:11 +01:00
Torsten Schulz (local)
8355f985cd Update calendar localization and enhance CalendarView: Add new entries, edit options, selection info, and form fields in both English and German localization files. Improve CalendarView component with new event dialog and selection handling for a better user experience. 2026-01-30 14:17:04 +01:00
Torsten Schulz (local)
25af538c88 Enhance calendar functionality in personal section: Implement detailed calendar views (month, week, workweek, day) in CalendarView component. Update localization files for English and German to include translations for calendar features such as views, weekdays, and months. This update improves user experience by providing a comprehensive calendar interface. 2026-01-30 14:07:28 +01:00
Torsten Schulz (local)
d1503cd813 Implement personal section in navigation: Add a new 'personal' menu item with sub-items for language learning and calendar. Update localization files for English and German to include translations for the new section. Integrate personal routes into the router configuration for navigation. 2026-01-30 13:53:27 +01:00
Torsten Schulz (local)
7d2a33b3ec Refactor DashboardWidget to use dynamic widget components: Replace static slot content with a dynamic component rendering based on the endpoint prop. This change simplifies the widget structure and enhances flexibility by allowing different widget types to be displayed. Additionally, update error handling to provide more specific error messages. 2026-01-30 13:42:22 +01:00
Torsten Schulz (local)
752686e3e1 Remove unused drag-and-drop indicators in LoggedInView: Eliminate placeholder elements and associated styles for drop zones to streamline the widget rearrangement interface. This change simplifies the code and enhances clarity in the drag-and-drop functionality. 2026-01-30 13:38:35 +01:00
Torsten Schulz (local)
3870f34ef8 Refactor age display logic in DashboardWidget: Simplify age representation to always show years, removing the conditional check for days. This change streamlines the age display functionality. 2026-01-30 13:34:02 +01:00
Torsten Schulz (local)
ae71a066c7 Enhance drag-and-drop functionality in LoggedInView: Update event handling to include event parameters for improved debugging and clarity. Add console logs to track drag-and-drop actions, enhancing the user experience during widget rearrangement. 2026-01-30 13:29:01 +01:00
Torsten Schulz (local)
b52327db2e Refine drag-and-drop functionality in LoggedInView: Simplify event handling for drag-and-drop actions by removing unnecessary parameters and updating placeholder comments for clarity. This change aims to enhance user experience during widget rearrangement. 2026-01-30 13:15:03 +01:00
Torsten Schulz (local)
d5c089e07e Refine drag-and-drop behavior in LoggedInView: Update drop event handling to prevent event propagation and improve widget insertion logic. Adjust drop target determination to enhance user experience during widget rearrangement. 2026-01-30 12:03:12 +01:00
Torsten Schulz (local)
0f78c624b1 Improve drag-and-drop indicators in LoggedInView: Enhance visual cues for widget insertion by updating placeholder comments and adding event handlers for drag-and-drop actions. This update aims to provide clearer instructions for users during widget rearrangement. 2026-01-30 11:44:08 +01:00
Torsten Schulz (local)
e1632c41c2 Enhance drag-and-drop experience in DashboardWidget: Implement a visual drag ghost image during widget dragging for improved user feedback. Adjust dragging state styles, including opacity and border, to better indicate active dragging. This update aims to refine the overall interaction and visual cues during widget rearrangement. 2026-01-30 11:13:51 +01:00
Torsten Schulz (local)
323b051355 Refine DashboardWidget and LoggedInView styles: Adjust transition effects and dragging state in DashboardWidget for improved visual feedback. Update grid row height in LoggedInView for better responsiveness and enhance drop indicator styling for a more polished user experience. 2026-01-30 10:44:46 +01:00
Torsten Schulz (local)
3999b17e88 Enhance drag-and-drop functionality in Dashboard: Update styles for dragging state in DashboardWidget, including opacity and box-shadow adjustments. Improve LoggedInView by adding drop indicators for better user experience during widget rearrangement. Refactor drag-and-drop logic to maintain visual cues and ensure smoother interactions. 2026-01-30 10:30:07 +01:00
Torsten Schulz (local)
8fd15614af Update age representation in DashboardWidget: Modify falukantAgeLabel computed property to display age in days if less than a year, otherwise show in years. Add localization for "days" in both German and English language files to support the new age format. 2026-01-30 10:05:30 +01:00
Torsten Schulz (local)
ddefc2737b Adjust dashboard grid row height in LoggedInView for improved layout: Change grid-auto-rows from 420px to 200px to enhance responsiveness and better accommodate dynamic content. 2026-01-30 09:54:55 +01:00
Torsten Schulz (local)
05868d8a09 Update DashboardWidget and LoggedInView styles: Adjust min-height and max-height properties for better layout flexibility. Enhance dashboard grid cell styling to support dynamic content display and improve overall UI responsiveness. 2026-01-30 09:26:00 +01:00
Torsten Schulz (local)
b3afb988a3 Improve AppNavigation item handling: Update the logic to only expand items with non-empty children, enhancing user experience by preventing unnecessary interactions on items without submenus. 2026-01-30 09:05:55 +01:00
Torsten Schulz (local)
3b8e0573f2 Enhance DashboardWidget functionality: Integrate Vuex state management for socket connections, enabling real-time updates for Falukant widgets. Refactor computed properties and methods to handle socket events and improve data fetching logic. Update localization for age representation and adjust styles for better UI presentation. 2026-01-30 08:18:50 +01:00
Torsten Schulz (local)
4779a6e4af Refactor dashboard widget management: Update dashboardService to handle user-specific widget configurations with create and update logic. Enhance LoggedInView to support adding the same widget type and display error messages for save operations. Ensure effective endpoint handling for widgets and improve UI interactions. 2026-01-30 07:31:38 +01:00
Torsten Schulz (local)
39ac149430 Enhance character name display in DashboardWidget: Refactor character name construction logic in FalukantService to include title handling and create a new computed property for display name in DashboardWidget. Update styles for gender display and ensure proper localization for age representation. 2026-01-29 17:28:58 +01:00
Torsten Schulz (local)
8ec7db031b Refactor DashboardWidget and LoggedInView: Update DashboardWidget to display a single news article instead of a list, enhancing user experience. Remove logout button and related functionality from LoggedInView to streamline the interface. 2026-01-29 17:26:43 +01:00
Torsten Schulz (local)
25b5b91a19 Fix conditional rendering in DashboardWidget: Change v-else-if to v-if for newsDataResults to ensure proper display of news articles when data is available. 2026-01-29 17:22:53 +01:00
Torsten Schulz (local)
e8c6f6ffb9 Add news widget functionality: Integrate newsRouter for fetching news data, update initializeWidgetTypes to include news endpoint, and enhance DashboardWidget component to display news articles with pagination support. Update LoggedInView to manage widget request counters for unique endpoint handling. 2026-01-29 17:20:06 +01:00
Torsten Schulz (local)
62d8cd7b05 Add dashboard widget functionality: Implement getDashboardWidget method in FalukantService to retrieve compact user data for the dashboard. Update FalukantController and router to expose the new endpoint, and enhance DashboardWidget component to display user-specific information including character name, gender, age, money, unread notifications, and children count. 2026-01-29 17:03:32 +01:00
Torsten Schulz (local)
c09159d6ce Add widget management functionality: Implement getAvailableWidgets method in dashboardService to retrieve widget types, and create corresponding API endpoint in dashboardRouter. Update LoggedInView to allow users to select and add widgets dynamically, enhancing dashboard customization options. 2026-01-29 16:57:12 +01:00
Torsten Schulz (local)
8d2db95540 Add dashboard functionality: Integrate dashboardRouter and UserDashboard model, enabling user-specific dashboard configurations. Update LoggedInView to support dynamic widget management, including adding, removing, and saving widget configurations, enhancing user experience and interactivity. 2026-01-29 16:52:54 +01:00
Torsten Schulz (local)
9519846489 Refactor inventory handling in FalukantService: Update product transport logic to retrieve all stocks for a branch, improving inventory checks and ensuring accurate transport cost calculations. Implement locking during inventory updates to maintain data integrity during transactions. 2026-01-29 16:44:02 +01:00
Torsten Schulz (local)
f7a977df33 Enhance FalukantService with character caching and optimized city retrieval: Introduce caching for cities with branch types to reduce database queries, and streamline character retrieval logic. Update product and knowledge fetching to improve performance and maintainability. 2026-01-29 16:04:43 +01:00
Torsten Schulz (local)
f1717920b6 Add batch processing for product price retrieval: Implement getProductPricesInCitiesBatch method in FalukantService for handling multiple product price requests in a single API call. Update FalukantController and router to support new endpoint, and refactor RevenueSection and SaleSection components to utilize batch processing for improved performance and reduced API calls. 2026-01-29 15:58:31 +01:00
Torsten Schulz (local)
c5ab17ad99 Refactor FalukantService and SaleSection components: Optimize product and knowledge retrieval by using Promise.all for concurrent database queries, improving performance. Additionally, reorganize the speedLabel method for better readability and maintainability, ensuring accurate localization handling for transport speed values. 2026-01-29 15:20:38 +01:00
Torsten Schulz (local)
1839c3c57b Refactor price loading logic in RevenueSection and SaleSection components: Replace for-loops with Promise.all for concurrent API requests, improving performance and responsiveness. This change enhances the handling of loading states and ensures better management of price data retrieval. 2026-01-29 15:12:13 +01:00
Torsten Schulz (local)
ba63b3504f Refactor SaleSection component: Introduce methods section to organize component logic, enhancing readability and maintainability. This change improves the structure of the component by clearly separating computed properties and methods. 2026-01-29 15:11:16 +01:00
Torsten Schulz (local)
032e336b65 Add synchronous price calculation method: Introduce calcRegionalSellPriceSync for improved performance in price calculations when worthPercent is known. Refactor getAllProductPricesInRegion to utilize this new method, enhancing efficiency by reducing database calls. Update BranchView to manage product prices cache with regionId for better data handling. 2026-01-29 15:06:38 +01:00
Torsten Schulz (local)
474e46837a Refactor speedLabel method across components: Enhance localization handling for speed values by adding support for object types and improving fallback logic. This change ensures more accurate translations and better user experience in displaying transport speed information. 2026-01-29 14:22:25 +01:00
Torsten Schulz (local)
e7052636ba Remove unused logging for queries in check-knowledge-pkey.js to streamline output and enhance clarity in database performance diagnostics. 2026-01-29 14:06:43 +01:00
Torsten Schulz (local)
cb2631061e Enhance database performance diagnostics: Add detailed logging for unused primary key index and implement error handling for query statistics retrieval. Additionally, automate ANALYZE execution for affected tables after index creation to ensure PostgreSQL optimizes query performance. This improves clarity on index usage and enhances overall database performance management. 2026-01-29 14:05:24 +01:00
Torsten Schulz (local)
d1ddfe7d31 Refactor database performance diagnostics: Update connection pool diagnostics in diagnose-db-performance.js to improve error handling and enhance console output clarity. This change ensures more accurate user feedback regarding database connection pooling. 2026-01-29 14:03:15 +01:00
Torsten Schulz (local)
59cad22183 Refactor connection pool diagnostics in diagnose-db-performance.js: Update pool configuration retrieval to use Sequelize config, improve error handling, and enhance console output for better clarity on pool status. This change ensures more accurate diagnostics and user feedback regarding database connection pooling. 2026-01-29 13:56:46 +01:00
Torsten Schulz (local)
57d64a7ef8 Refactor database performance diagnostics: Update SQL queries in diagnose-db-performance.js to use 'relname' instead of 'tablename' for improved accuracy in table and index size calculations. This change enhances the clarity and correctness of performance diagnostics. 2026-01-29 13:55:07 +01:00
Torsten Schulz (local)
ae096eb4c3 Update StatusBar component: Refactor rendering logic for status items, improve icon and label display, and enhance CSS for better alignment and styling. This change aims to boost visual clarity and overall user experience in the status bar. 2026-01-29 13:29:42 +01:00
Torsten Schulz (local)
789861999c Refactor StatusBar component: Improve status item rendering by enhancing icon and label display, and update CSS for better alignment and styling. This change enhances the visual clarity and user experience of the status bar. 2026-01-29 11:58:15 +01:00
Torsten Schulz (local)
72f4bd066d Enhance MoneyHistoryGraphDialog: Add axis labels for better data visualization, improve graph scaling logic, and implement money and date formatting methods. Update localization files to include "yesterday" translations in German and English, enhancing user experience and clarity in financial data representation. 2026-01-29 11:05:56 +01:00
Torsten Schulz (local)
b3db65d1b8 Add money history graph feature: Implement moneyHistoryGraph method in FalukantService and corresponding controller and router updates. Enhance frontend with a new dialog for displaying money history over various time ranges, including localization updates for German and English. This improves user experience by providing visual insights into financial data. 2026-01-29 10:40:13 +01:00
Torsten Schulz (local)
506a9cd9c0 Fix localization key in NobilityView: Update the translation key for the next title display to ensure correct localization in the advance section, enhancing user experience. 2026-01-29 09:45:48 +01:00
Torsten Schulz (local)
1ead06fd4f Add reputation actions and localization updates: Implement getReputationActions method in FalukantService, enhancing reputation management. Update German and English localization files to include new reputation action terms and mood descriptions, improving user experience and clarity. 2026-01-29 09:37:34 +01:00
Torsten Schulz (local)
eecd947377 Enhance date formatting in NobilityView: Update formatDate method to display time for today's or future dates, improving user experience by providing more relevant information. 2026-01-29 09:26:21 +01:00
Torsten Schulz (local)
5351e3ea57 Refactor NobilityView: Remove debug logs and improve conditional rendering for advance section. Add cooldown message styling for better user feedback when advancement is not possible. 2026-01-29 09:23:24 +01:00
Torsten Schulz (local)
3bdb77888f Enhance NobilityView with debugging information: Add conditional rendering for advance section based on availability of next title information. Implement detailed console logs for loading and advancing nobility data, improving error handling and debugging capabilities. 2026-01-29 09:20:10 +01:00
Torsten Schulz (local)
c570fd6ae3 Refactor association keys in setupAssociations: Update foreign key naming conventions in Party and TitleOfNobility associations for consistency and clarity. Enhance error handling in FalukantService to ensure party ID is present before adding invited nobilities. 2026-01-29 09:05:38 +01:00
Torsten Schulz (local)
be3ed4af5d Add reputation actions endpoint: Implement getReputationActions method in FalukantService, update FalukantController to wrap the new method, and add corresponding route in falukantRouter for retrieving reputation actions. 2026-01-29 08:59:00 +01:00
Torsten Schulz (local)
4cce044128 Refactor character name handling in enrichNotificationsWithCharacterNames: Update comments for clarity on character_name and characterName usage. Enhance localization by adding success messages in German and English translations. 2026-01-29 07:46:17 +01:00
Torsten Schulz (local)
59875cf900 Implement migration to make church_application supervisor_id nullable: Enhance functionality by allowing supervisor_id to be null, improving flexibility in application processing. Add logging for migration success and error handling. 2026-01-28 17:07:11 +01:00
Torsten Schulz (local)
37129055e6 Update supervisorId handling in ChurchApplication and FalukantService: Allow supervisorId to be null for entry-level positions, enhancing flexibility in application processing. Improve prerequisite office type updates in initializeFalukantTypes for better data integrity. 2026-01-28 17:02:27 +01:00
Torsten Schulz (local)
934e80c2ab Enhance logging and validation in getAvailableChurchPositions: Add detailed console logs for office type requirements and user qualifications, improving debugging and ensuring correct handling of prerequisite office types. 2026-01-28 16:57:46 +01:00
Torsten Schulz (local)
8e20fbd24d Add church office requirements validation in FalukantService: Implement checks for prerequisite office types when determining available positions, enhancing the application process for church offices based on user qualifications. 2026-01-28 16:52:44 +01:00
Torsten Schulz (local)
f102069f5a Remove unused supervisor assignment in FalukantService: Clean up code by eliminating the conditional block that sets the supervisor object, streamlining the availablePositions logic. 2026-01-28 16:47:57 +01:00
Torsten Schulz (local)
afc36161ed Add region relationship to ChurchOffice model: Establish associations between ChurchOffice and RegionData, allowing for better organization of church offices by region. Enhance logging in getChurchOverview method for improved debugging and error handling. 2026-01-28 16:45:40 +01:00
Torsten Schulz (local)
a8b76bc21a Add church management features: Implement endpoints for church overview, available positions, supervised applications, and application processing in FalukantService and FalukantController. Update router to include new routes for these functionalities, enhancing church-related operations. 2026-01-28 16:41:19 +01:00
Torsten Schulz (local)
8550bd31d9 Add bulk pricing retrieval for products in region: Implement getAllProductPricesInRegion method in FalukantService, update FalukantController and router to support new endpoint, and modify BranchView to utilize bulk request for improved performance. 2026-01-28 15:18:26 +01:00
Torsten Schulz (local)
8837494a06 Refactor ProductType model: Remove unused sellCostMinNeutral and sellCostMaxNeutral fields for cleaner code and improved maintainability. 2026-01-28 15:13:39 +01:00
Torsten Schulz (local)
0c407b81b7 Enhance error handling in FalukantService: Add validation for product sellCost in calcRegionalSellPrice and getProductPriceInRegion methods, ensuring proper error messages are logged when sellCost is undefined or null. 2026-01-28 15:05:28 +01:00
Torsten Schulz (local)
71b4a02592 Update FalukantService to include character ID in attributes and enhance logging in PoliticsView for better debugging of character data retrieval. 2026-01-28 14:58:11 +01:00
Torsten Schulz (local)
83e5767812 Enhance logging in PoliticsView: Add detailed console logs for API responses and error handling in loadCurrentPositions and loadOwnCharacterId methods. Improve isOwnPosition method with additional logging for better debugging and clarity. 2026-01-28 14:56:07 +01:00
Torsten Schulz (local)
c544c2c7f9 Add holder ID to character object in FalukantService: Include the holder's ID in the character data structure to enhance user identification and data integrity. 2026-01-28 14:52:08 +01:00
Torsten Schulz (local)
818c8fbdf9 Implement own position highlighting in PoliticsView: Add functionality to load and display the user's own character ID, enhancing the UI by highlighting positions associated with the user. Update styles for better visibility of own positions in the table. 2026-01-28 14:47:20 +01:00
Torsten Schulz (local)
a6326f149d Add lay-preacher office and update prerequisites in FalukantTypes; enhance church localization in English and German 2026-01-28 14:24:24 +01:00
Torsten Schulz (local)
01679697b4 Remove debug logging from getFalukantUserByHashedId and getChildren methods in FalukantService for cleaner code and improved performance. 2026-01-28 13:44:12 +01:00
Torsten Schulz (local)
d4fb2a8ccc Enhance health activity error handling: Implement detailed error responses in FalukantController for 'tooClose' scenarios, including retry timing. Update localization files for improved user feedback on health measures and errors. Refactor error handling in HealthView to display appropriate messages based on error responses. 2026-01-28 13:34:42 +01:00
Torsten Schulz (local)
08b6437a1e Improve error handling in FalukantController: Enhance response structure for error objects by including additional error data while maintaining status code integrity. This change allows for more informative error messages in the API response. 2026-01-28 13:29:15 +01:00
Torsten Schulz (local)
baffd9d05c Update English localization file: Reorganize JSON structure for improved readability and maintainability, ensuring consistent formatting across keys and values. 2026-01-28 11:55:27 +01:00
Torsten Schulz (local)
cbff7c130c Füge Funktion zum Abbrechen der Werbung hinzu: Implementiere cancelWooing in FalukantService und FalukantController, aktualisiere FamilyView für die Benutzeroberfläche und verbessere die Fehlermeldungen bei vorzeitigen Abbrüchen. 2026-01-28 11:53:34 +01:00
Torsten Schulz (local)
16f3d1a320 Verbessere Fehlerbehandlung beim Geschenkeversand: Füge Unterstützung für Retry-Nachricht und verbessere die Lokalisierung der Fehlermeldungen in FamilyView hinzu. 2026-01-26 17:15:36 +01:00
Torsten Schulz (local)
955ea1a9ed Enhance gift sending logic: Implement retry mechanism for 'tooOften' error in FalukantService and update error handling in FamilyView to display retry time. 2026-01-26 16:45:13 +01:00
Torsten Schulz (local)
ca614f6cc2 Enhance database connection management by introducing configurable pool settings and implementing a retry mechanism for transient connection issues. Updated Sequelize pool options to use environment variables for better flexibility and added a retry wrapper for initializing settings to handle connection timeouts gracefully. 2026-01-26 16:27:36 +01:00
Torsten Schulz (local)
71748f6aa0 Refactor SaleSection component: Simplify sell item and sell all logic, remove unnecessary state management, and improve UI feedback. Update translations and clean up unused code in i18n files. Optimize price loading in BranchView and remove legacy product loading in MoneyHistoryView. Streamline PoliticsView by removing own character ID handling and related logic. 2026-01-26 16:03:48 +01:00
Torsten Schulz (local)
80b639b511 Remove deprecated relationship change log migration and enhance error handling in FalukantService for partner retrieval 2026-01-26 10:10:22 +01:00
Torsten Schulz (local)
bba68da488 Add RelationshipChangeLog model and enhance character loading logic 2026-01-26 09:54:40 +01:00
Torsten Schulz (local)
29c2b53f53 Refactor VocabLessonView route definition for improved readability
- Updated the routing configuration to directly reference the VocabLessonView component, enhancing code clarity and consistency in component usage.
2026-01-23 17:19:41 +01:00
Torsten Schulz (local)
c3cc248a39 Add VocabLessonView component to socialRoutes
- Imported VocabLessonView component and updated the route definition to use the imported component directly instead of a dynamic import.
- This change enhances code readability and maintains consistency in how components are referenced in the routing configuration.
2026-01-23 14:42:21 +01:00
Torsten Schulz (local)
fb821dbf21 Update color scheme across components to use new primary orange variable
- Replaced hardcoded orange color values with the new CSS variable for primary orange in multiple components, ensuring consistency in styling.
- Updated styles in DialogWidget, MessageboxWidget, SettingsWidget, SimpleTabs, and various Falukant components to enhance maintainability and readability.
2026-01-23 13:56:19 +01:00
Torsten Schulz (local)
079250fcd7 Add notification messages for office filling in German and English locales
- Added new notification messages for when a political office is filled in both the German and English locale files.
- Ensured consistency in messaging across different languages to enhance user experience.
2026-01-23 13:53:32 +01:00
Torsten Schulz (local)
120cb5fadd Refactor and reintroduce character name enrichment logic in FalukantService
- Moved the enrichNotificationsWithCharacterNames function back into the FalukantService class, ensuring character names are correctly parsed and attached to notifications.
- Implemented a comprehensive ID collection and resolution mechanism for character references, enhancing the accuracy of character name assignments.
- Improved error handling during JSON parsing and ID resolution to maintain robustness in notification processing.
2026-01-23 13:46:06 +01:00
Torsten Schulz (local)
d3a554108f Enhance syncModelsAlways function to handle problematic ENUM changes
- Added logic to skip synchronization for specific tables with known ENUM issues, improving stability during model synchronization.
- Restored associations and virtual fields for models when skipping sync, ensuring data integrity and consistency.
2026-01-23 13:39:14 +01:00
Torsten Schulz (local)
6471158847 Add connection management scripts to package.json
- Introduced new scripts: check-connections and cleanup-connections for managing database connections.
- These scripts enhance the backend's ability to monitor and maintain connection health.
2026-01-23 13:35:33 +01:00
Torsten Schulz (local)
1c442eb195 Update Sequelize configuration for improved connection management
- Reduced connection pool size and adjusted timeout settings to prevent connection limits.
- Added automatic reconnection handling for better resilience against connection losses.
- Introduced retry logic for specific connection errors to enhance stability during database interactions.
2026-01-23 13:28:31 +01:00
Torsten Schulz (local)
13f5660fee Add sync-tables script to package.json for table synchronization 2026-01-23 13:18:46 +01:00
Torsten Schulz (local)
9333a8318c Enhance query handling and foreign key management in sequelize.js
- Updated queryWithTimeout to support parameter replacements, improving query flexibility.
- Enhanced foreign key checks in syncModelsAlways to handle timeouts and errors more gracefully, ensuring robust logging and skipping problematic checks.
- Implemented a check for table existence before synchronization for large tables, preventing unnecessary sync operations and improving performance.
2026-01-23 13:13:35 +01:00
Torsten Schulz (local)
c1cda5fa62 Enhance Sequelize configuration and query handling in sequelize.js
- Added connection pool settings to optimize database connection management.
- Introduced a queryWithTimeout helper function to handle long-running queries, improving error handling and preventing indefinite hangs.
- Updated syncModelsAlways function to utilize queryWithTimeout for foreign key checks and cleanup operations, enhancing robustness and logging for better visibility during synchronization.
2026-01-23 12:48:26 +01:00
Torsten Schulz (local)
88967ba9d3 Implement model synchronization with timeout handling in sequelize.js
- Added a helper function to synchronize models with a timeout, improving error handling for long-running sync operations.
- Updated the syncModelsAlways function to utilize the new timeout feature, providing better control over model synchronization and preventing indefinite hangs.
- Enhanced logging to indicate model sync progress and timeout occurrences, improving visibility during deployment.
2026-01-23 10:37:47 +01:00
Torsten Schulz (local)
92d792246c Enhance database cleanup operations in syncDatabase.js
- Added a helper function to check for table existence before performing cleanup operations, ensuring that invalid queries are avoided.
- Updated cleanup logic for church_office and church_application tables to only execute if the respective tables exist, improving robustness and preventing errors.
- Maintained existing logging for cleanup operations to provide visibility into the process.
2026-01-23 09:42:49 +01:00
Torsten Schulz (local)
586aaec506 Add queryWithTimeout helper for database operations in syncDatabase.js
- Introduced a new helper function to execute database queries with a timeout, improving error handling for long-running queries.
- Updated multiple cleanup operations to utilize the new helper, enhancing code readability and maintainability.
- Added descriptive logging for each cleanup operation to provide better insights into the database synchronization process.
2026-01-22 17:18:27 +01:00
Torsten Schulz (local)
10690b5a6e Optimize database cleanup process in syncDatabase.js
- Enhanced orphaned entry cleanup queries with LEFT JOIN for improved performance.
- Added logging for each cleanup step to provide better visibility into the process.
- Included additional cleanup for church_office and church_application tables to remove invalid entries.
- Updated vehicle condition handling to set legacy NULLs to 100 and clamp values between 0 and 100.
2026-01-22 17:07:04 +01:00
Torsten Schulz (local)
bceef9777a Refactor church career validation in FalukantService
- Moved church career checks to a more logical position in the transaction flow.
- Improved error handling by ensuring character existence is validated before checking for church office.
- Cleaned up commented code for better readability and maintainability.
2026-01-22 16:54:29 +01:00
Torsten Schulz (local)
4f786cdcc3 Implement church career management features
- Added endpoints for church career functionalities including overview, available positions, application submission, and application decision-making.
- Enhanced the FalukantController to handle church-related requests.
- Updated associations and models to support church office types and requirements.
- Integrated new routes in the falukantRouter for church career operations.
- Implemented service methods for managing church applications and checking church career status.
- Updated frontend components to display current positions, available positions, and manage applications with appropriate UI elements and loading states.
- Localized new church-related strings in both English and German.
2026-01-22 16:46:42 +01:00
Torsten Schulz (local)
8e226615eb Refactor character avatar rendering in OverviewView.vue
- Replaced 3D character rendering with a 2D avatar display for improved performance.
- Introduced dynamic avatar styling based on user gender and age group.
- Added computed properties for avatar and house styles to enhance visual representation.
- Cleaned up CSS to support the new avatar display structure.
2026-01-22 16:02:41 +01:00
Torsten Schulz (local)
82734e8383 Refactor source directory handling in modelsProxyService.js
- Introduced a cached source directory variable to optimize the retrieval of model paths.
- Updated comments for clarity on the source directory logic and its impact on cache validation.
2026-01-22 15:50:46 +01:00
Torsten Schulz (local)
69a83c584b Enhance model path handling in modelsProxyService.js
- Refactored model source directory logic to dynamically select between production and local paths.
- Updated error messages to provide clearer context on model source lookup failures.
- Added package-lock.json to .gitignore to streamline dependency management.
2026-01-22 13:50:47 +01:00
Torsten Schulz (local)
a8fdcd179e Remove package-lock.json files from frontend, backend, and root directories to streamline dependency management and reduce repository size. 2026-01-22 13:50:38 +01:00
Torsten Schulz (local)
ace976965d Refactor model path handling in Character3D component
- Introduced a constant for the models API path to streamline model path construction.
- Updated modelPath method to utilize the new constant, improving code clarity and maintainability.
- Adjusted fallback model path logic to ensure consistent API usage.
2026-01-22 13:42:27 +01:00
Torsten Schulz (local)
7303d1ea0b Implement request handling for 3D models in app.js
- Added middleware to prevent direct access to /models/ paths, directing users to use /api/models/ instead for optimized 3D models.
- Updated comments to clarify the purpose of the new middleware and its role in serving models through the API.
2026-01-22 13:40:03 +01:00
Torsten Schulz (local)
4379b0b955 Implement model optimization and caching for 3D characters
- Added a new modelsProxyRouter to handle requests for optimized 3D character models.
- Introduced modelsProxyService to manage GLB file optimization using gltf-transform with Draco compression.
- Updated app.js to include the new modelsProxyRouter for API access.
- Enhanced .gitignore to exclude model cache files.
- Added scripts for optimizing GLB models and updated README with optimization instructions.
- Integrated DRACOLoader in Character3D.vue for loading compressed models.
- Updated FamilyView.vue to streamline character rendering logic.
2026-01-22 13:24:47 +01:00
Torsten Schulz (local)
09af7af228 Update color scheme in AppHeader and AppNavigation components for visual consistency
- Changed background color in AppHeader.vue and AppNavigation.vue to a new shade for improved aesthetics.
- Adjusted hover states and submenu background colors in AppNavigation.vue to match the updated theme.
- Refactored FamilyView.vue to enhance layout with additional padding for better alignment of elements.
2026-01-22 12:56:53 +01:00
Torsten Schulz (local)
dc08da211f Update AppHeader and FamilyView components for improved styling and layout
- Changed background color in AppHeader.vue for enhanced visual appeal.
- Refactored FamilyView.vue to improve layout by introducing a flexbox structure for better alignment of relationship details and 3D character models.
- Added new CSS classes to support the updated layout and ensure responsive design.
2026-01-22 12:49:48 +01:00
Torsten Schulz (local)
30e1df0dd8 Enhance deployment scripts and frontend components for improved functionality and styling
- Added dependency installation step in deploy-frontend.sh and update-frontend.sh to ensure all required packages are available before building the frontend.
- Updated AppNavigation.vue to change background color for better visual appeal.
- Refactored FamilyView.vue to include 3D character models for both the user and their relationships, enhancing the visual representation of family dynamics.
- Modified OverviewView.vue to switch from 3D character rendering to a 2D avatar display, improving loading performance and user experience.
2026-01-22 12:39:24 +01:00
Torsten Schulz (local)
95a4c977c1 Update Vite configuration for improved module resolution and build settings
- Changed the import path for GLTFLoader to align with the latest Three.js structure.
- Added preserveSymlinks option to the build configuration for better symlink handling.
- Updated Rollup options to explicitly define no external modules, enhancing build clarity.
2026-01-22 12:31:20 +01:00
Torsten Schulz (local)
6ce081196c Update Vite configuration and Character3D component for improved module handling
- Added 'three' to the dedupe array in Vite config to optimize dependency resolution.
- Updated CommonJS options to enable transformation of mixed ES modules for better compatibility.
- Changed the import path for GLTFLoader in Character3D.vue to align with the latest Three.js structure.
2026-01-22 12:26:52 +01:00
Torsten Schulz (local)
3d5342b314 Update Vite configuration to enhance dependency optimization and build settings
- Added 'three' and its GLTFLoader to the optimizeDeps include array for improved module resolution.
- Introduced custom Rollup options to prevent externalization of the 'three' library during the build process.
- Configured CommonJS options to include 'three' and node_modules for better compatibility with dependencies.
2026-01-22 12:24:57 +01:00
Torsten Schulz (local)
78d43e6859 Update color palette and styles across components for improved visual consistency
- Changed theme color in index.html to a brighter orange for better aesthetics.
- Introduced a modern color palette in styles.scss for enhanced readability and consistency.
- Updated various components (AppFooter, AppNavigation, DialogWidget, etc.) to utilize new color variables, ensuring a cohesive look throughout the application.
- Adjusted button styles and hover effects for improved user interaction feedback.
- Enhanced background colors and text colors for better contrast and visibility.
2026-01-22 12:22:05 +01:00
Torsten Schulz (local)
41106ae306 Add 3D character rendering to Character3D component
- Integrated Three.js for 3D character visualization based on user gender and age.
- Simplified the character structure by removing outdated HTML elements and replacing them with a dynamic 3D model loader.
- Implemented model loading with fallback options and added animation capabilities for enhanced visual appeal.
- Updated CSS for the character container to ensure proper rendering and responsiveness.
2026-01-22 11:53:40 +01:00
Torsten Schulz (local)
33aa2ddd45 Refactor OverviewView and NoLoginView to integrate Character3D component
- Replaced avatar display logic in OverviewView with a 3D character representation based on user gender and age.
- Updated NoLoginView to utilize Character3D for displaying mascots, enhancing visual consistency.
- Removed outdated avatar positioning logic and related computed properties for improved code clarity and maintainability.
- Adjusted CSS styles for better layout and responsiveness of character displays.
2026-01-22 11:06:38 +01:00
Torsten Schulz (local)
2be5505c55 Refactor MessagesDialog.vue for improved readability and functionality
- Replaced hardcoded button definitions with a computed property for better maintainability.
- Enhanced pagination logic by introducing a reset function and simplifying page navigation methods.
- Improved notification handling by restructuring parameter extraction and formatting, ensuring better clarity and consistency in displaying messages.
- Updated CSS styles for better visual presentation and consistency across the component.
2026-01-22 10:25:00 +01:00
Torsten Schulz (local)
8c0f07cc51 Optimize falukantService and DirectorInfo component for improved performance and user experience
- Refactored proposal handling in falukantService to load existing proposals before cleaning expired ones, reducing unnecessary database queries.
- Enhanced fetchProposals method with explicit joins for better performance and added a limit to avoid excessive data retrieval.
- Updated DirectorInfo component to reload data after hiring a director, ensuring the UI reflects the latest information.
2026-01-22 10:15:45 +01:00
Torsten Schulz (local)
3018b1f2e1 Refactor updateFoodCareExercises function to optimize database queries and improve code clarity
- Replaced the Sequelize findAll method with a raw SQL query to fetch Bisaya courses along with native language information, enhancing performance.
- Updated variable names for consistency and clarity, ensuring better readability of the code.
- Adjusted the handling of course owner IDs to align with the new query structure, improving data integrity.
2026-01-21 13:22:03 +01:00
Torsten Schulz (local)
a21a2314d7 Enhance survival sentences exercises and VocabCourseView for improved user experience
- Added multiple choice exercises for common phrases in Bisaya, including questions and explanations to aid learning.
- Introduced a gap fill exercise for completing survival sentences, enhancing interactive learning.
- Updated VocabCourseView to display a button for continuing the current lesson, ensuring smoother navigation.
- Implemented logic to prevent starting lessons without completing previous ones, improving course progression management.
2026-01-21 12:01:31 +01:00
Torsten Schulz (local)
a76aae3d12 Update falukantService and MessagesDialog for improved data handling and parameter extraction
- Changed the join in falukantService from 'title_of_nobility' to 'title' for better clarity in data relationships.
- Enhanced MessagesDialog.vue to directly extract parameters from parsed data when no value object is present, improving the handling of character-related parameters and ensuring backend names take precedence.
2026-01-21 08:33:59 +01:00
Torsten Schulz (local)
7765067d1b Refine distractor selection logic in VocabLessonView to enhance vocabulary options
- Updated the logic for generating distractors by collecting unique vocabulary entries from both learning and reference directions, ensuring no duplicates are included.
- Increased the maximum attempts for finding distractors and adjusted fallback mechanisms to ensure a minimum of two options are provided, improving the overall user experience.
2026-01-20 15:14:50 +01:00
Torsten Schulz (local)
eddbe5fa3f Refine vocabulary selection logic in VocabLessonView to prevent duplicates
- Updated the condition for adding vocabulary options to ensure that normalized vocabulary entries are checked against the exclusion set, preventing duplicates and enhancing the learning experience.
2026-01-20 15:09:37 +01:00
Torsten Schulz (local)
c907d2773d Enhance Bisaya course content and VocabLessonView for improved clarity and functionality
- Updated gap fill exercises in create-bisaya-course-content.js to include clearer instructions and contextual hints for better understanding.
- Refined the logic in VocabLessonView.vue to prevent duplicate vocabulary entries and ensure only distinct translations are added, enhancing the learning experience.
- Adjusted timing for transitioning between questions in VocabLessonView to improve user interaction flow.
2026-01-20 15:05:48 +01:00
Torsten Schulz (local)
5f71e56bf9 Update VocabCourseView to simplify lesson number display and improve table styling
- Changed the lesson number header to a simple "#" for clarity.
- Adjusted column widths for the lesson number to enhance layout consistency.
- Centered text alignment for lesson number cells to improve visual presentation.
2026-01-20 15:01:12 +01:00
Torsten Schulz (local)
adcbd1a95a Refactor VocabCourseView layout for improved structure and styling
- Introduced wrapper divs for lesson title, status, and actions to enhance layout organization and readability.
- Updated CSS styles to ensure consistent display and alignment of lesson elements, improving overall user experience.
- Enhanced flexbox usage for better responsiveness and visual clarity in the lessons table.
2026-01-20 14:50:09 +01:00
Torsten Schulz (local)
175a61c81c Enhance VocabService and VocabCourseView for improved multiple choice handling and table layout
- Updated VocabService to support multiple correct answers in multiple choice exercises, allowing for better answer validation and user feedback.
- Enhanced the extraction of correct answers and alternatives to accommodate both single and multiple correct indices.
- Improved CSS styles in VocabCourseView for better table layout, including adjustments for overflow handling and vertical alignment, enhancing overall user experience.
2026-01-20 14:46:07 +01:00
Torsten Schulz (local)
4d97f24531 Update VocabCourseView styles for improved table layout and responsiveness
- Changed table layout to 'separate' for better spacing and visual clarity.
- Adjusted column widths and added min/max width properties for lesson number, title, status, and actions to ensure consistent display.
- Enhanced CSS properties for table rows to manage text overflow and improve overall user experience.
2026-01-20 14:38:17 +01:00
Torsten Schulz (local)
8d32d704b5 Enhance exercise generation for family conversations and feelings & affection
- Updated multiple choice exercises to include randomized wrong options for improved engagement and challenge.
- Added new exercise types for reading aloud and speaking from memory, enhancing interactive learning experiences.
- Improved gap fill exercises with clearer instructions and multiple variants for better user understanding.
- Enhanced the vocabulary service to support new exercise types, ensuring robust answer checking and feedback mechanisms.
- Updated localization files to include new instructions and messages related to the new exercise types.
2026-01-20 14:30:19 +01:00
Torsten Schulz (local)
e5d4a5f95f Refactor VocabCourseView to enhance lesson display and user interaction
- Replaced the lesson item layout with a structured table format for improved readability and organization of lesson information.
- Updated lesson status indicators to include completion badges and scores, providing clearer feedback on progress.
- Enhanced action buttons with distinct styles for better user experience and accessibility.
- Improved CSS styles for a more modern and responsive design, ensuring a consistent look and feel across the application.
2026-01-20 14:16:22 +01:00
Torsten Schulz (local)
d4a0f78cd0 Implement watchers for courseId and lessonId in VocabLessonView to reset flags on changes
- Added watchers for courseId and lessonId to reset flags related to lesson completion and navigation when either value changes, ensuring proper lesson loading and state management.
- Removed redundant method definitions for courseId and lessonId, streamlining the component's code structure and improving maintainability.
2026-01-20 00:14:05 +01:00
Torsten Schulz (local)
7cd946181e Refactor _extractVocabFromExercises method for improved array handling
- Enhanced the method to better handle various input types, including array-like objects, with detailed console logging for conversion attempts and errors.
- Implemented a fallback mechanism for manual conversion of non-array inputs, ensuring robustness in vocabulary extraction.
- Updated comments for clarity on input expectations and processing logic, improving maintainability.
2026-01-20 00:10:51 +01:00
Torsten Schulz (local)
cf97a3ba5e Enhance _extractVocabFromExercises method for robust input handling
- Added checks to ensure the exercises parameter is an array, with console warnings for null, undefined, or non-array inputs.
- Implemented logic to convert non-array objects with a length property into arrays, improving flexibility in handling various input types.
- Enhanced error logging to provide clearer insights when input conversion fails, aiding in debugging and user feedback.
2026-01-20 00:00:57 +01:00
Torsten Schulz (local)
963e0c906c Refactor updateFamilyConversationExercises for improved database queries and clarity
- Replaced direct model queries with raw SQL queries for fetching the Bisaya language and associated courses, enhancing performance and readability.
- Simplified the retrieval of native language information for courses by using a single SQL query with a LEFT JOIN, reducing the number of database calls.
- Updated variable names for better clarity and consistency in the codebase.
2026-01-19 23:40:54 +01:00
Torsten Schulz (local)
089743ac23 Refactor VocabLessonView for improved lesson navigation and user feedback
- Enhanced the loadLesson and checkLessonCompletion methods to streamline lesson transitions and prevent redundant executions, improving user experience.
- Updated navigation logic to utilize more efficient history management, ensuring smoother course and lesson changes.
- Added detailed console logging for better insights during lesson loading and completion checks, aiding in debugging and user interaction.
2026-01-19 23:37:16 +01:00
Torsten Schulz (local)
69ef120677 Enhance VocabService and VocabLessonView for review lesson functionality
- Added logic in VocabService to retrieve vocabulary and lessons from previous lessons for review sessions, improving the learning experience.
- Implemented methods to gather review lessons and vocabulary exercises, ensuring users have access to relevant content during review lessons.
- Updated VocabLessonView to utilize review vocabulary exercises when in a review lesson, enhancing vocabulary extraction and user feedback.
- Improved console logging for better insights into the vocabulary processing flow, aiding in debugging and user interaction.
2026-01-19 23:33:45 +01:00
Torsten Schulz (local)
fe2e6a53e9 Implement dialog prompts for lesson navigation and error handling in VocabLessonView
- Removed the manual check answer button, transitioning to automatic answer verification upon option selection.
- Added dialog overlays for navigating to the next lesson, course completion notifications, and error messages, enhancing user interaction.
- Introduced methods to handle dialog confirmations and cancellations, improving the flow of lesson transitions and error management.
- Updated styles for dialog components to ensure a consistent and user-friendly interface.
2026-01-19 23:28:45 +01:00
Torsten Schulz (local)
cf1b5e7f71 Update VocabLessonView to enhance vocabulary mapping and logging for language exercises
- Refined vocabulary mapping logic to better reflect the relationship between native words and their translations, improving clarity in exercise generation.
- Enhanced console logging to provide more detailed insights into the vocabulary patterns being processed, aiding in debugging and user feedback.
- Updated comments to clarify the purpose of each pattern and the expected input/output, ensuring better maintainability of the code.
2026-01-19 23:14:18 +01:00
Torsten Schulz (local)
202002358a Enhance transformation exercise formatting and improve choice option generation in VocabLessonView
- Added formatting for transformation exercises to display prompts as "Übersetze 'X' ins Bisaya", improving clarity for users.
- Updated the buildChoiceOptions method to exclude the prompt from the choice options, ensuring a more relevant selection for multiple choice questions.
- Enhanced console logging to provide better insights into the prompt and answer during choice option creation, aiding in debugging and user feedback.
2026-01-19 23:10:01 +01:00
Torsten Schulz (local)
14eb28d37f Refactor family vocabulary exercises generation for improved flexibility
- Replaced static family vocabulary exercises with a dynamic structure that generates exercises based on native language input, enhancing adaptability for different languages.
- Introduced a mapping for family words and their translations, allowing for a more comprehensive and organized approach to exercise creation.
- Updated logging to include native language context during exercise generation, improving clarity for developers and users.
- Streamlined the gap fill and transformation exercises to ensure accurate and relevant content for learners.
2026-01-19 22:59:46 +01:00
Torsten Schulz (local)
81dbbdd6f5 Add family vocabulary exercises in Bisaya course content
- Introduced a new set of multiple choice and gap fill exercises focused on family-related vocabulary in Bisaya, enhancing language learning.
- Included detailed explanations for each term to provide context and aid understanding for learners.
- Removed dummy exercises for unknown lessons, streamlining the exercise return logic for better user experience.
2026-01-19 22:48:10 +01:00
Torsten Schulz (local)
9e6787fb3f Enhance logging and improve vocabulary trainer functionality in VocabLessonView
- Added detailed console logging to track the flow of the vocabulary trainer, including checks for available vocabulary and question generation.
- Implemented a safeguard against infinite loops when generating choice options by limiting attempts.
- Enhanced the buildChoiceOptions method to ensure a minimum number of options, adding generic choices if necessary.
- Improved the nextVocabQuestion method with additional logging for better debugging and user feedback.
2026-01-19 22:33:12 +01:00
Torsten Schulz (local)
2eee7bb0c1 Enhance logging and prevent redundant execution in VocabLessonView
- Added console logging to provide better feedback during lesson loading and completion checks.
- Improved loadLesson and checkLessonCompletion methods to prevent multiple executions and enhance user experience.
- Updated error handling to include more descriptive logging for easier debugging.
2026-01-19 22:27:22 +01:00
Torsten Schulz (local)
7f57ecc35e Refactor lesson loading and navigation logic in VocabLessonView
- Improved handling of course and lesson changes by resetting flags to prevent multiple executions during navigation.
- Enhanced the loadLesson method to prevent redundant loading and ensure a smoother user experience.
- Added console logging for better debugging and user feedback during navigation and lesson transitions.
- Updated navigation logic to use replace instead of push for better history management.
2026-01-19 22:15:06 +01:00
Torsten Schulz (local)
21f6130666 Enhance lesson completion checks and navigation in VocabLessonView
- Introduced flags to prevent multiple executions of lesson completion checks and navigation, improving user experience and preventing potential issues.
- Updated the checkLessonCompletion method to ensure accurate score calculation and progress updates.
- Enhanced the navigateToNextLesson method to handle course loading and user navigation more effectively, including user prompts for lesson completion.
- Improved the typing mode logic to ensure questions are only presented when available, enhancing the vocabulary trainer functionality.
2026-01-19 22:01:30 +01:00
Torsten Schulz (local)
594b3dac4a Refactor Bisaya course exercises for clarity and consistency
- Updated gap fill and multiple choice exercises to streamline content and improve user understanding.
- Simplified question structures and reduced the number of gaps in exercises for better engagement.
- Enhanced explanations for phrases to provide clearer context and meaning for learners.
2026-01-19 21:57:15 +01:00
Torsten Schulz (local)
ef2b279df6 Refactor exercise handling and improve user feedback in VocabLessonView
- Enhanced the exercise display logic to provide clearer feedback on available grammar exercises, improving user engagement.
- Updated conditional rendering to ensure accurate handling of lesson and exercise data, reducing potential errors.
- Improved logging for exercise counts and active tab states, aiding in debugging and performance monitoring.
2026-01-19 21:52:13 +01:00
Torsten Schulz (local)
2ffd7a6151 Add new survival phrases and exercises in Bisaya course content
- Introduced a comprehensive set of survival phrases in Bisaya, including multiple choice and gap fill exercises to enhance language learning.
- Added detailed explanations for each phrase to aid understanding and context for learners.
- Structured content into two parts for better organization and accessibility, ensuring a progressive learning experience.
- Updated existing exercises to include new vocabulary and improve user engagement with practical language use.
2026-01-19 21:31:18 +01:00
Torsten Schulz (local)
045d32c245 Enhance VocabLessonView with new vocabulary trainer features and improved statistics
- Added functionality for tracking total attempts and success rates in the vocabulary trainer, enhancing user feedback on performance.
- Introduced multiple choice and typing modes for vocabulary practice, allowing users to switch between different learning styles.
- Updated translations in both English and German to include new vocabulary terms and exercise instructions, ensuring consistency across languages.
- Improved UI layout for displaying vocabulary statistics and answer options, enhancing overall user experience.
2026-01-19 21:23:13 +01:00
Torsten Schulz (local)
053588ae74 Refactor hasExercises computed property in VocabLessonView for improved validation
- Enhanced the hasExercises method to include comprehensive checks for lesson and grammarExercises, ensuring robust validation before accessing properties.
- Removed redundant computed property declaration to streamline the code structure.
- Added additional logging for important vocabulary and grammar explanations to aid in debugging and provide better insights during lesson loading.
2026-01-19 21:10:20 +01:00
Torsten Schulz (local)
749a2d6f59 Refactor vocabulary extraction logic in VocabLessonView for multiple choice and gap fill exercises
- Enhanced the handling of multiple choice questions by extracting options and determining the correct answer index, improving accuracy in vocabulary mapping.
- Updated the question analysis to support different patterns for extracting German and Bisaya words, enhancing the learning experience.
- Improved gap fill answer extraction by iterating through possible answers and creating unique keys for vocabulary mapping, ensuring better context handling.
- Added detailed error logging to assist in debugging vocabulary extraction issues.
2026-01-19 21:04:01 +01:00
Torsten Schulz (local)
95ba8f0b33 Enhance VocabLessonView with vocabulary trainer and grammar explanations
- Introduced a vocabulary trainer feature allowing users to practice important vocabulary interactively, with options to start and stop the trainer.
- Added sections for grammar explanations and lesson descriptions to improve user understanding of the content.
- Updated translations in both English and German to reflect changes in vocabulary and exercise terminology.
- Enhanced conditional rendering to ensure proper display of vocabulary and grammar information based on lesson data.
2026-01-19 20:58:39 +01:00
Torsten Schulz (local)
dacf6cb7f8 Add vocabulary information text and improve conditional rendering in VocabLessonView
- Introduced a new translation key for vocabulary information text in both English and German, providing context for users on vocabulary usage in exercises.
- Enhanced conditional rendering in VocabLessonView to ensure lesson data is properly checked before displaying cultural notes and vocabulary lists, improving robustness and user experience.
2026-01-19 19:43:59 +01:00
Torsten Schulz (local)
656c3b3d09 Improve exercise display and logging in VocabLessonView
- Updated the exercise tab button to show the count of available grammar exercises, enhancing user feedback.
- Enhanced conditional rendering to ensure proper handling of lesson and exercise data, preventing potential errors.
- Added detailed logging for exercise count and active tab state, aiding in debugging and monitoring.
2026-01-19 19:40:36 +01:00
Torsten Schulz (local)
44ce6636c0 Refactor answer checking logic in VocabService to support multiple exercise types
- Updated the _checkAnswer method to handle both multiple choice and gap fill exercises more effectively.
- Enhanced the extraction of correct answers and alternatives based on exercise type, improving accuracy in answer validation.
- Added JSON parsing for answer and question data to ensure compatibility with various input formats.
- Improved fallback mechanisms for answer checking to accommodate different data structures.
2026-01-19 19:35:41 +01:00
Torsten Schulz (local)
1413630f11 Fix layout issue in VocabLessonView by closing a div tag for improved rendering of exercise details 2026-01-19 19:15:36 +01:00
Torsten Schulz (local)
8f55f63f77 Enhance logging and conditional rendering in VocabService and VocabLessonView
- Added detailed logging in VocabService for lesson retrieval, including lesson ID, title, and exercise count.
- Improved conditional rendering in VocabLessonView to handle cases where grammar exercises may not be present, enhancing user experience.
- Updated logging in VocabLessonView to provide insights into loaded lessons and exercises, aiding in debugging and monitoring.
2026-01-19 19:12:54 +01:00
Torsten Schulz (local)
0331ffeb93 Improve error handling and validation in importantVocab computed property of VocabLessonView
- Added checks to ensure importantVocab is only processed if lesson and grammarExercises are valid.
- Enhanced error handling with try-catch blocks to log issues during vocabulary extraction, improving robustness.
- Updated the condition for rendering the vocabulary list to prevent errors when importantVocab is undefined.
2026-01-19 19:01:49 +01:00
Torsten Schulz (local)
196b74bebb Enhance VocabLessonView and VocabService with new learning features
- Added a tabbed interface in VocabLessonView for 'Learn' and 'Exercises' sections, improving user navigation.
- Implemented logic to display important vocabulary and cultural notes in the learning section.
- Updated exercise result display to include correct answers and alternatives for better user feedback.
- Enhanced VocabService to extract correct answers and alternatives from exercise data, supporting the new UI features.
- Added new translations for vocabulary-related terms in both English and German, ensuring consistency across the application.
2026-01-19 16:41:10 +01:00
Torsten Schulz (local)
305e137a1a Reset exercise answers and results in VocabLessonView before loading new lessons
- Added logic to clear previous exercise answers and results when loading a new lesson, ensuring accurate tracking of user responses.
- Simplified the handling of exercise answers and results by directly assigning values instead of using Vue's `$set` method, improving code readability.
2026-01-19 16:08:39 +01:00
Torsten Schulz (local)
4e5ddc8027 Enhance VocabLessonView and VocabService for grammar exercise handling
- Added logic to initialize grammar exercises directly from lesson data or load them separately if not included.
- Introduced a new method to initialize answer arrays for gap fill exercises, improving user interaction and response tracking.
- Updated comments for clarity on the exercise loading process and initialization logic.
2026-01-19 15:33:15 +01:00
Torsten Schulz (local)
4bb75de3f0 Enhance grammar exercise functionality in VocabLessonView
- Added support for multiple exercise types including multiple choice, gap fill, and transformation.
- Updated UI to display exercise instructions and results with improved styling.
- Implemented logic to handle answer checking based on exercise type, enhancing user interaction.
- Added new translations for exercise-related terms in both English and German.
2026-01-19 15:23:16 +01:00
Torsten Schulz (local)
0572a0eb50 Add grammar exercise creation in course generation
- Integrated functionality to create example grammar exercises for grammar lessons during course creation.
- Added a new helper function to generate gap fill and multiple choice exercises based on lesson data.
- Enhanced logging to confirm the number of grammar exercises created, improving feedback during course setup.
2026-01-19 15:15:24 +01:00
Torsten Schulz (local)
c13cb40c7b Add lesson retrieval functionality in VocabController and VocabService
- Introduced a new method in VocabService to fetch lesson details, including access control based on user ownership and lesson visibility.
- Updated VocabController to wrap the new method for user access.
- Added a new route in VocabRouter to handle requests for specific lessons.
- Enhanced VocabCourseListView to support navigation to individual lesson views, improving user experience in accessing lesson content.
2026-01-19 15:07:52 +01:00
Torsten Schulz (local)
33787ba796 Refactor VocabCourseListView for improved layout and styling
- Updated the course item structure to enhance visual clarity and organization.
- Introduced new CSS classes for better styling and hover effects on course items.
- Adjusted padding, margins, and gaps for a more cohesive design.
- Improved button styles for enrollment, continuation, and editing actions, enhancing user interaction.
2026-01-19 14:27:41 +01:00
Torsten Schulz (local)
64f4468664 Add endpoint to retrieve all available languages in VocabController and VocabRouter
- Introduced a new method in VocabService to list all languages from the database.
- Updated VocabController to wrap the new method for user access.
- Added a new route in VocabRouter to handle requests for all languages.
- Modified VocabCourseListView to utilize the new endpoint for loading languages, enhancing the course selection experience.
2026-01-19 14:23:37 +01:00
Torsten Schulz (local)
408b65be30 Refactor native language loading in VocabCourseListView for improved clarity and error handling
- Renamed loadMyNativeLanguage to loadMyNativeLanguageId for better context.
- Enhanced error handling to log warnings when the languages list is empty or when the native language is not found.
- Improved debug logging to provide clearer insights into the native language loading process.
2026-01-19 14:11:22 +01:00
Torsten Schulz (local)
891420cb09 Refactor VocabService to improve direct property handling and enhance language loading
- Updated VocabService to calculate direct where properties after setting all direct properties, ensuring accurate query conditions.
- Enhanced filtering of AND conditions to remove empty objects before assignment, improving query efficiency.
- Added logic to load native language names for courses, ensuring accurate mapping of language IDs to names.
- Improved comments for clarity on the new logic and its implications on course retrieval.
2026-01-19 14:07:16 +01:00
Torsten Schulz (local)
a657c59b2c Add support for user's native language in VocabCourseListView
- Introduced a new option for selecting the user's native language in the course selection dropdown.
- Implemented logic to load the user's native language based on the UI language and map it to the corresponding language ID.
- Updated internationalization files to include strings for the user's native language in both German and English, enhancing the user experience.
2026-01-19 13:54:03 +01:00
Torsten Schulz (local)
89ec084106 Refactor VocabService to improve boolean parameter handling and enhance debugging
- Updated VocabService to convert string parameters for course retrieval into booleans, ensuring accurate filtering based on user input.
- Added detailed debug logging to track the state of query conditions and the final WHERE clause, aiding in troubleshooting and performance analysis.
- Improved comments for clarity on the logic and implications of the changes made.
2026-01-19 13:52:27 +01:00
Torsten Schulz (local)
a7a0daaf82 Enhance VocabService to combine AND conditions in query filtering
- Updated VocabService to combine AND conditions with direct where properties when both are present, improving query accuracy.
- Added comments for better understanding of the new logic and its implications on course retrieval.
2026-01-19 13:14:13 +01:00
Torsten Schulz (local)
df5c2a3141 Enhance VocabService logging and update VocabCourseListView state management
- Added debug logging in VocabService to track course retrieval details, aiding in troubleshooting and performance monitoring.
- Updated VocabCourseListView to include additional state properties for managing share codes and search functionality, improving user experience and interaction capabilities.
2026-01-19 13:03:06 +01:00
Torsten Schulz (local)
f902f5298c Refactor native language filtering in VocabService and update frontend handling
- Simplified the logic for filtering courses by native language in VocabService, allowing for better handling of undefined and null values.
- Enhanced the VocabCourseListView to clarify the behavior of nativeLanguageId based on user selection, ensuring accurate course retrieval based on language preferences.
- Improved comments in both files for better understanding of the filtering logic and its implications on course visibility.
2026-01-19 12:09:48 +01:00
Torsten Schulz (local)
ddd038761b Enhance language course creation script to support public courses
- Updated the script to create public language courses for various target and native languages without requiring an ownerHashedId.
- Implemented a function to find or create a system user for course ownership, ensuring automatic user assignment.
- Improved documentation to clarify the script's usage and the types of courses created.
2026-01-19 11:47:55 +01:00
Torsten Schulz (local)
09e53244d9 Add native language support in vocab course management
- Introduced a new field for native language in the VocabCourse model to allow learners to specify their native language.
- Updated the VocabService to handle native language during course creation and retrieval, including filtering options.
- Enhanced the database schema to include foreign key constraints for native language.
- Updated frontend components to support native language selection and display in course listings.
- Added internationalization strings for native language features in both German and English.
2026-01-19 11:43:38 +01:00
Torsten Schulz (local)
714e144329 Add course retrieval by share code feature and enhance course search functionality
- Implemented a new endpoint in VocabController to retrieve courses using a share code.
- Updated VocabService to include logic for validating share codes and checking course access permissions.
- Enhanced course listing functionality with search and language filtering options in the frontend.
- Added a dialog for users to input share codes and search for courses, improving user experience.
- Updated internationalization files to include new strings for share code functionality and search features.
2026-01-19 11:33:20 +01:00
Torsten Schulz (local)
e1b3dfb00a Refactor navigation structure to enhance language learning features
- Renamed 'vocabtrainer' to 'sprachenlernen' in the navigation structure for better clarity.
- Introduced a nested structure under 'sprachenlernen' for 'vocabtrainer' and 'sprachkurse', improving organization of language-related resources.
- Updated internationalization files for both German and English to reflect the new naming and structure, ensuring consistency across the application.
2026-01-19 11:24:46 +01:00
Torsten Schulz (local)
b6a4607e60 Implement vocab course and grammar exercise features in backend and frontend
- Added new course management functionalities in VocabController, including creating, updating, and deleting courses and lessons.
- Implemented enrollment and progress tracking for courses, along with grammar exercise creation and management.
- Updated database schema to include tables for courses, lessons, enrollments, and grammar exercises.
- Enhanced frontend with new routes and views for course listing and details, including internationalization support for course-related texts.
- Improved user experience by adding navigation to courses from the main vocab trainer view.
2026-01-19 10:58:53 +01:00
Torsten Schulz (local)
9553cc811a Update index.html and sitemap.xml for improved SEO and content visibility
- Updated the title and description in index.html to include new features like Vokabeltrainer and additional minigames.
- Added keywords meta tag to enhance search engine optimization.
- Modified sitemap.xml to reflect changes in URLs and update change frequencies for better indexing, including new entries for Vokabeltrainer and minigames.
2026-01-19 10:35:59 +01:00
Torsten Schulz (local)
59c05b3628 Implement job hierarchy and region depth calculations in FalukantService; enhance PoliticsView with own position highlighting
- Added a job hierarchy mapping to determine positions based on their rank.
- Introduced asynchronous region depth calculations to determine the hierarchy of regions.
- Updated the mapping of office data to include job hierarchy levels and region depths.
- Enhanced the PoliticsView to highlight the user's own positions with a distinct style.
- Implemented a method to load the user's character ID for position comparison.
2026-01-16 16:25:22 +01:00
Torsten Schulz (local)
d3629a8a09 Enhance character name resolution logic in MessagesDialog component
- Improved character name extraction by prioritizing names resolved from the backend over those from notification values.
- Simplified fallback mechanisms for character names, ensuring a more robust handling of character IDs.
- Streamlined the logic for handling character names in various effect types, enhancing clarity and maintainability of the code.
2026-01-15 14:08:02 +01:00
Torsten Schulz (local)
a17e8537fb Enhance character name resolution in enrichNotificationsWithCharacterNames function
- Introduced sets for collecting first name and last name IDs to improve character name enrichment.
- Implemented batch loading of first names and last names, optimizing database queries.
- Added a helper function to resolve names from character first and last name IDs, enhancing notification data with resolved character names.
- Improved logic for attaching resolved names to notifications, prioritizing name resolution from IDs over fallback methods.
2026-01-15 13:33:54 +01:00
Torsten Schulz (local)
a7f23c5885 Refactor update-backend.sh script for improved .env file handling and backup process
- Updated the script to securely back up .env files before deleting the old backend, ensuring no loss of environment configurations.
- Enhanced the restoration process of .env files with clearer logging for success and warnings when no files are found.
- Simplified the logic for copying the new backend while excluding .env files, improving clarity and maintainability of the script.
2026-01-15 13:11:44 +01:00
Torsten Schulz (local)
b706191a0e Refactor effect handling in enrichNotificationsWithCharacterNames to improve data parsing
- Simplified the logic for processing notification effects by consolidating the JSON parsing into a single conditional check, enhancing code readability and maintainability.
- Ensured consistent handling of effects regardless of their initial format, improving robustness in notification enrichment.
2026-01-15 09:28:00 +01:00
Torsten Schulz (local)
ba469ef900 Refactor notification handling in FalukantService to improve data processing
- Convert notification rows to plain objects before enriching with character names, ensuring compatibility with subsequent processing.
- Update return structure to include plain notification objects, enhancing data consistency in the response.
2026-01-15 08:16:04 +01:00
Torsten Schulz (local)
e852346b94 Update mood handling in FalukantService and enhance Socket.io configuration in store
- Modified mood assignment in FalukantService to conditionally wrap mood data in an object based on moodId presence, improving data structure consistency.
- Refactored Socket.io initialization in the store to dynamically set the secure option based on the URL scheme, enhancing connection security in production environments.
2026-01-14 16:22:05 +01:00
Torsten Schulz (local)
02d24eccd8 Add 'sleep' status to Production model and update related components
- Introduced a new 'sleep' boolean field in the Production model to indicate if production is suspended.
- Updated FalukantService to include 'sleep' in the production attributes.
- Enhanced MessagesDialog and ProductionSection components to display the production status and handle branch names.
- Added corresponding translations for 'status', 'sleep', and 'active' in both German and English locale files.
2026-01-14 15:29:53 +01:00
Torsten Schulz (local)
d1359ccc36 Refactor proposal mapping in FalukantService to handle null characters
- Updated the proposal mapping logic to filter out proposals with null proposed characters, enhancing data integrity.
- Ensured that noble title is safely accessed with a fallback to null, improving robustness in character data handling.
2026-01-14 14:58:59 +01:00
Torsten Schulz (local)
52c7f1c7ba Refactor sendMessageToConnection method to enhance user data validation and message handling
- Introduce a local copy of the message to ensure its validity during processing.
- Validate user data retrieved from the WebSocket interface to ensure consistency before queuing messages.
- Streamline logging by removing redundant checks and focusing on critical error handling, improving overall clarity and stability.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
7a2749c405 Remove redundant exception handling in sendMessageToConnection method to streamline error logging and improve code clarity. 2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
d71df901ed Refactor message sending logic in WebSocket server to improve direct transmission and error handling
- Attempt to send messages directly during the RECEIVE callback to avoid mutex issues, enhancing performance.
- Implement size checks for messages to prevent overflow, with logging for oversized messages.
- Introduce additional error handling and logging for socket write operations, ensuring robust message delivery and queue management.
- Maintain thread safety by validating user data and mutex locking before queuing messages when direct sending fails.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
1af4b6c2e4 Enhance logging and error handling in sendMessageToConnection method
- Introduce detailed logging for the message sending process, including checks for user data validity and message queue status.
- Implement additional null checks for user data before and after locking the mutex to ensure thread safety.
- Ensure proper message copying to maintain validity during queuing, improving overall stability and error visibility.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
2595cb8565 Enhance error handling and logging in WebSocket server message sending
- Introduce detailed logging for message creation and sending processes, including message size and success confirmation.
- Implement comprehensive null checks for instance, WebSocket interface, and user data before invoking sendMessageToConnection, improving stability.
- Add exception handling to capture and log errors during message sending, enhancing visibility into potential issues.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
45d549aa4e Refactor message sending in WebSocket server to utilize sendMessageToConnection
- Replace manual message queuing and error handling with the sendMessageToConnection method, which consolidates necessary checks and improves code clarity.
- Remove redundant null checks and logging related to message queue access, streamlining the callback logic.
- Enhance overall stability by leveraging existing functionality for message delivery during WebSocket events.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
7f65f5e40e Enhance message queuing with improved error handling and logging in WebSocket server
- Implement detailed logging for message queuing attempts, including message size and copy operations.
- Add comprehensive null checks for user data and message queue validity before pushing messages to the queue.
- Introduce exception handling to manage potential errors during message queuing, improving stability and error visibility.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
5ce1cc4e6a Refine null checks and logging in WebSocket server message handling
- Introduce a local copy of user data before locking the mutex to ensure validity during message queuing.
- Enhance null checks and logging to provide clearer insights when user data or message queue access fails.
- Implement exception handling for message queue access to improve stability and error visibility.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
3a6d60e9a8 Improve null checks and logging in WebSocket server message handling
- Add additional null checks for user data before and after locking the mutex to prevent potential crashes.
- Enhance logging to provide clearer insights when user data is invalid during message queuing.
- Ensure proper message copying to a local variable before accessing the message queue, improving thread safety and stability.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
d5a09f359d Enhance logging and error handling in getConnections callback
- Add detailed logging to track the flow and validity of user data during the getConnections event.
- Implement exception handling to manage potential access issues with user data, improving stability and error visibility.
- Ensure clear output for both successful and failed user data access attempts, aiding in debugging and monitoring.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
127e95ca1c Improve null checks and logging in WebSocket server callbacks
- Add checks for user data to prevent null pointer exceptions during message handling.
- Enhance logging to provide clearer insights when user data is invalid or when exceptions occur.
- Ensure proper mutex locking when accessing the message queue to maintain thread safety.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
bb81126cd8 Enhance error response handling in WebSocket server
- Add detailed logging for error responses during WebSocket callbacks, improving visibility into the error handling process.
- Ensure that error responses are queued correctly without immediate sending, enhancing stability during callback execution.
- Utilize lws_cancel_service to notify the service of pending messages, ensuring proper message delivery after error handling.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
2d3d120f81 Refactor WebSocket server message queuing and error handling
- Implement message queuing for error responses during WebSocket callbacks to prevent immediate sending, enhancing stability.
- Utilize lws_cancel_service to trigger the writable callback safely, ensuring messages are sent correctly after the callback execution.
- Improve error handling and logging for message sending operations, providing clearer insights into potential issues.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
0c36c4a4e5 Refactor WebSocket server message handling to include user data
- Update sendMessageToConnection to accept user data, enhancing message delivery accuracy.
- Improve error handling in WebSocket callbacks by adding user data checks to prevent null pointer exceptions.
- Enhance logging for error responses to provide clearer insights into message handling issues.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
88f6686809 Enhance WebSocket server message handling and error responses
- Improve asynchronous message sending to prevent connection issues during callbacks.
- Add error response handling for failed connection retrieval, ensuring clients receive feedback on errors.
- Implement message size checks to prevent oversized messages from being sent, enhancing stability and reliability.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
9c7b682a36 Improve error handling and null checks in WebSocket server callbacks
- Add null checks for user data in various WebSocket callback functions to prevent crashes and improve stability.
- Enhance error logging to provide clearer insights into issues related to user data and connection management.
- Refactor the handling of active connections to ensure robust error handling during data processing and message sending.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
dafdbf0a84 Refactor WebSocket server to use nlohmann::json for active connections
- Update the return type of getActiveConnections() in both websocket_server.cpp and websocket_server.h to nlohmann::json for consistency and clarity.
- Ensure proper usage of the nlohmann::json library in the WebSocket server implementation.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
5ac8e9b484 Enhance WebSocket server connection management and error handling
- Introduce connection time tracking for WebSocket users to monitor connection duration.
- Implement user ID management to allow dynamic updates and removal of connections based on user ID changes.
- Add functionality to retrieve active connections, including unauthenticated ones, for administrative purposes.
- Improve error handling during connection closure and ensure proper cleanup of connection entries.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
753c5929e1 Refactor configuration file installation and template handling
- Update CMakeLists.txt to install the template configuration file as an example, ensuring it is available for reference.
- Modify install-config.cmake to prioritize the installed template file, with fallbacks to source directory templates if the installed one is missing, enhancing the robustness of the configuration setup.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
e3f46d775a Enhance WebSocket server ping/pong handling and timeout settings
- Introduce handling for LWS_CALLBACK_RECEIVE_PONG to manage Pong frames received from clients.
- Update the WebSocketUserData structure to increase MAX_PING_TIMEOUTS from 3 to 5, allowing more attempts before disconnection.
- Extend PONG_TIMEOUT_SECONDS from 10 to 60 to accommodate longer response times from browsers.
- Modify ping handling to send a WebSocket Ping frame instead of a text message for better protocol compliance.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
0eb3a78332 Enhance configuration file installation process
- Implement a CMake script for intelligent merging of configuration files, ensuring only missing keys are added without overwriting existing ones.
- Install a template configuration file as an example, preventing overwriting of the original during installation.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
3ac9f25284 Enhance WebSocket server socket options and connection handling
- Add support for setting SO_REUSEADDR in the WebSocket server to allow port reuse, improving server flexibility.
- Implement callbacks for socket adoption to ensure SO_REUSEADDR is set when applicable.
- Refine server options to streamline connection management and enhance overall performance.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
b3c9c8f37c Improve WebSocket server startup and error reporting
- Introduce a brief wait time to ensure the port is released before starting the server.
- Update server options to allow port reuse, enhancing server flexibility.
- Enhance error handling during context creation to provide more informative error messages regarding port usage and permissions.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
32bc126def Enhance WebSocket server options and error handling
- Update server options to support multiple simultaneous connections and improve security practices.
- Allow multiple connections per IP with configurable keep-alive settings.
- Improve error handling during WebSocket service operations, ensuring critical errors lead to server shutdown.
- Refine connection closure logic to handle user IDs more robustly and log connection states accurately.
- Enable WebSocket upgrade requests while rejecting other HTTP requests for better protocol management.
2026-01-14 14:38:43 +01:00
Torsten Schulz (local)
00a5f47cae Refactor WebSocket server connection management and message handling
- Update WebSocketUserData to use a message queue for handling outgoing messages, improving concurrency and message delivery.
- Modify pingClients method to handle multiple connections per user and implement timeout logic for ping responses.
- Enhance addConnection and removeConnection methods to manage multiple connections for each user, including detailed logging of connection states.
- Update handleBrokerMessage to send messages to all active connections for a user, ensuring proper queue management and callback invocation.
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
6a1260687b Implement comprehensive character deletion process in UserCharacterWorker
- Add queries and logic to delete associated data when a character dies, including directors, relationships, child relations, knowledge, debtors prism, political offices, and election candidates.
- Enhance error handling to log issues during the deletion process.
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
7591787583 Update configuration file path for daemon in main.cpp 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
bd961a03d4 Aktualisiere WebSocket-Server und Daemon-Konfiguration
- Ändere die Pfade für SSL-Zertifikate in der Konfigurationsdatei.
- Verbessere die Fehlerbehandlung beim Entfernen alter vorbereiteter Anweisungen in HouseWorker.
- Füge Debug-Ausgaben zur Nachverfolgung von Verbindungen und Nachrichten im WebSocket-Server hinzu.
- Implementiere Timeout-Logik für das Stoppen von Worker- und Watchdog-Threads.
- Optimiere die Signalverarbeitung und Shutdown-Logik in main.cpp für bessere Responsivität.
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8fe816dddc WebSocket-Verbindungsverwaltung implementiert
- User-ID wird bei setUserId Event gespeichert
- Verbindungen werden in connections Map verwaltet
- Nachrichten werden über pendingMessage gesendet
- Statische Instanz-Referenz für Callback-Zugriff
- Explizite JSON-Konvertierung für Kompatibilität
2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
e7a8dc86eb Füge Unterstützung für die Verwaltung von WebSocket-Verbindungen hinzu. Implementiere Methoden zum Hinzufügen und Entfernen von Verbindungen basierend auf Benutzer-IDs. Aktualisiere die WebSocket-Callback-Logik, um empfangene Nachrichten zu verarbeiten und Benutzer-IDs zu setzen. Verbessere die Ausgabe von Debug-Informationen zur Nachverfolgung von Verbindungen und Nachrichten. 2026-01-14 14:38:42 +01:00
Torsten (PC)
c9dc891481 updated rights 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
89c3873db7 Füge Überprüfung auf sudo-Rechte im SSL-Setup-Skript hinzu und aktualisiere die Pfade für Apache2-Zertifikate. Priorisiere Let's Encrypt-Zertifikate und füge Warnungen für Snakeoil-Zertifikate hinzu, um Benutzer über deren Einschränkungen zu informieren. Aktualisiere die Dokumentation entsprechend. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
60352d7932 Erweitere das SSL/TLS Setup-Skript um Unterstützung für Apache2-Zertifikate. Füge eine neue Funktion zum Einrichten und Verlinken von Apache2-Zertifikaten hinzu, einschließlich der Überprüfung auf vorhandene Zertifikate und der automatischen Erneuerung für Let's Encrypt. Aktualisiere die Benutzerführung zur Auswahl von Zertifikatstypen und dokumentiere die neuen Optionen in der SSL-Setup-Dokumentation. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
664f2af346 Erweitere das SSL/TLS Setup-Skript für den YourPart Daemon um Unterstützung für Let's Encrypt. Füge Funktionen zur Erstellung und Erneuerung von Let's Encrypt Zertifikaten hinzu, einschließlich automatischer Erneuerung über Cron Jobs. Ermögliche die Auswahl zwischen Self-Signed und Let's Encrypt Zertifikaten und verbessere die Benutzerführung bei der Zertifikatsauswahl. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8212e906a3 Füge Unterstützung für SSL/TLS in den WebSocket-Server hinzu. Aktualisiere die Konfigurationsdatei, um SSL-Optionen zu ermöglichen, und passe die WebSocketServer-Klasse an, um Zertifikat- und Schlüsselpfade zu akzeptieren. Verbessere die Serverstartlogik, um SSL korrekt zu initialisieren und entsprechende Meldungen auszugeben. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
92e17a9f43 Verbessere die Verwaltung der Konfigurationsdatei im Skript deploy-server.sh. Füge eine Überprüfung hinzu, ob die Konfigurationsdatei existiert, und kopiere sie nur, wenn sie nicht vorhanden ist. Ergänze die Logik zum Hinzufügen fehlender Schlüssel in die bestehende Konfigurationsdatei. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
d3727ad2f7 Ändere den Typ des Services in der Datei yourpart-daemon.service von 'simple' auf 'notify' und füge die Option NotifyAccess hinzu. Verbessere die Signalverarbeitung in main.cpp, um ein sauberes Herunterfahren der Anwendung zu ermöglichen und die Hauptschleife anzupassen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
391e5d9992 Ändere den Typ des Services in der Datei yourpart-daemon.service von 'notify' auf 'simple' für eine verbesserte Service-Verwaltung. Füge im Skript deploy-server.sh eine Verzögerung von 3 Sekunden nach dem Start des Services hinzu, um sicherzustellen, dass der Dienst ordnungsgemäß initialisiert wird. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
a4bd585730 Füge Überprüfung und Stopp des Services vor dem Kopieren der Dateien im Skript deploy-server.sh hinzu. Aktualisiere die Nummerierung der Schritte für eine bessere Übersichtlichkeit und entferne die Überprüfung, ob der Service bereits läuft, bevor er neu gestartet wird. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
c694769f4c Füge Überprüfung der Root-Rechte hinzu und aktualisiere Berechtigungen im Skript deploy-server.sh. Alle relevanten Operationen, die erhöhte Rechte benötigen, werden nun mit sudo ausgeführt, um die Sicherheit und Funktionalität zu verbessern. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8b9ff9793c Verbessere die Statusverarbeitung in der Methode spyIn, indem die Zuweisung des Status aus dem JSON-Objekt optimiert wird. Verwende nun die get-Methode für eine klarere und sicherere Zuweisung. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8ba4566d23 Füge Unterstützung für systemd hinzu, indem die systemd-Entwicklungslibraries in CMakeLists.txt und Installationsskripten für OpenSUSE und Ubuntu 22 integriert werden. Aktualisiere die yourpart-daemon.service-Datei für eine verbesserte Service-Verwaltung und implementiere die Benachrichtigung an systemd, wenn der Dienst bereit ist. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
91420b9973 Erweitere die Unterstützung für vorbereitete Abfragen in der Datenbank, indem die Methode exec_params für bis zu 10 Parameter implementiert wird. Füge eine Fehlerbehandlung für zu viele Parameter hinzu. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
8d3e0423e7 Füge Unterstützung für verschiedene Versionen von libpqxx hinzu, um die Kompatibilität mit libpqxx 6.x und 7.x zu gewährleisten. Implementiere unterschiedliche Methoden zur Ausführung vorbereiteter Abfragen basierend auf der Anzahl der Parameter. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
4bafc3a61c Verbessere die CMake-Konfiguration zur Unterstützung von C++23, indem die Compiler-Auswahl dynamisch auf GCC 15 oder 13 basiert. Optimiere die Compiler-Flags für Leistung. In der Datenbankabfrage und im DirectorWorker werden konstante Referenzen und string_view verwendet, um die Leistung zu steigern. Reserviere Speicher für Vektoren in main.cpp zur Effizienzsteigerung. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
1f43df6d41 Ändere die Berechtigungen des Skripts deploy-server.sh von 644 auf 755, um die Ausführbarkeit zu ermöglichen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
c2a54e29f8 Aktualisiere die Compiler-Version in CMakeLists.txt und install-dependencies-ubuntu22.sh von GCC 15 auf GCC 13 für bessere Unterstützung von C++23. Passe die Installationsmeldungen und Standard-Compiler-Einstellungen entsprechend an. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
b1f9073f4d Ändere die Berechtigungen des Skripts install-dependencies-ubuntu22.sh von 644 auf 755, um die Ausführbarkeit zu ermöglichen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
1b38e2412c Aktualisiere das Build-Skript in deploy.sh, um den C++ Standard auf Version 23 zu setzen. Ändere die Installation des C++ Compilers in install-dependencies-ubuntu22.sh, um GCC 15 zu installieren und als Standard-Compiler festzulegen. Entferne die vorherige Installation von GCC 11. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
4b9311713a Aktualisiere das Build-Skript, um C++ Standard auf Version 20 zu setzen. Ändere die Installation des C++ Compilers in install-dependencies.sh, um GCC 11 als Standard für Ubuntu 22 zu verwenden und entferne die Installation von GCC 15. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
77520ee46a Ändere die Berechtigungen der Skripte deploy.sh und install-dependencies.sh von 644 auf 755, um die Ausführbarkeit zu ermöglichen. 2026-01-14 14:38:42 +01:00
Torsten Schulz (local)
23c07a3570 Füge UndergroundWorker hinzu und implementiere Logik für unterirdische Aufgaben. Aktualisiere CMakeLists.txt, um neue Quell- und Header-Dateien einzuschließen. Verbessere die Fehlerbehandlung in der Datenbank und sende Benachrichtigungen nach bestimmten Ereignissen. Integriere Hilfsfunktionen zur sicheren Verarbeitung von Daten. 2026-01-14 14:38:35 +01:00
Torsten Schulz
1451225978 stabilized app 2026-01-14 14:37:21 +01:00
Torsten Schulz
51fd9fcd13 Ändere die Überprüfung auf die Erstellung von Charakteren von "vorheriger Tag" zu "heutiger Tag" 2026-01-14 14:37:21 +01:00
Torsten (PC)
1fe77c0905 fix 2026-01-14 14:36:57 +01:00
242 changed files with 32819 additions and 12586 deletions

7
.gitignore vendored
View File

@@ -5,6 +5,7 @@
.depbe.sh .depbe.sh
node_modules node_modules
node_modules/* node_modules/*
**/package-lock.json
backend/.env backend/.env
backend/images backend/images
backend/images/* backend/images/*
@@ -17,3 +18,9 @@ frontend/dist
frontend/dist/* frontend/dist/*
frontedtree.txt frontedtree.txt
backend/dist/ backend/dist/
backend/data/model-cache
build
build/*
.vscode
.vscode/*
.clang-format

156
CHURCH_MODELS.md Normal file
View File

@@ -0,0 +1,156 @@
# Church Models - Übersicht für Daemon-Entwicklung
## 1. ChurchOfficeType (falukant_type.church_office_type)
**Schema:** `falukant_type`
**Tabelle:** `church_office_type`
**Zweck:** Definiert die verschiedenen Kirchenämter-Typen
```javascript
{
id: INTEGER (PK, auto-increment)
name: STRING (z.B. "pope", "cardinal", "lay-preacher")
seatsPerRegion: INTEGER (Anzahl verfügbarer Plätze pro Region)
regionType: STRING (z.B. "country", "duchy", "city")
hierarchyLevel: INTEGER (0-8, höhere Zahl = höhere Position)
}
```
**Beziehungen:**
- `hasMany` ChurchOffice (als `offices`)
- `hasMany` ChurchApplication (als `applications`)
- `hasMany` ChurchOfficeRequirement (als `requirements`)
---
## 2. ChurchOfficeRequirement (falukant_predefine.church_office_requirement)
**Schema:** `falukant_predefine`
**Tabelle:** `church_office_requirement`
**Zweck:** Definiert Voraussetzungen für Kirchenämter
```javascript
{
id: INTEGER (PK, auto-increment)
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
prerequisiteOfficeTypeId: INTEGER (FK -> ChurchOfficeType.id, nullable)
minTitleLevel: INTEGER (nullable, optional)
}
```
**Beziehungen:**
- `belongsTo` ChurchOfficeType (als `officeType`)
- `belongsTo` ChurchOfficeType (als `prerequisiteOfficeType`)
---
## 3. ChurchOffice (falukant_data.church_office)
**Schema:** `falukant_data`
**Tabelle:** `church_office`
**Zweck:** Speichert tatsächlich besetzte Kirchenämter
```javascript
{
id: INTEGER (PK, auto-increment)
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
characterId: INTEGER (FK -> FalukantCharacter.id)
regionId: INTEGER (FK -> RegionData.id)
supervisorId: INTEGER (FK -> FalukantCharacter.id, nullable)
createdAt: DATE
updatedAt: DATE
}
```
**Beziehungen:**
- `belongsTo` ChurchOfficeType (als `type`)
- `belongsTo` FalukantCharacter (als `holder`)
- `belongsTo` FalukantCharacter (als `supervisor`)
- `belongsTo` RegionData (als `region`)
---
## 4. ChurchApplication (falukant_data.church_application)
**Schema:** `falukant_data`
**Tabelle:** `church_application`
**Zweck:** Speichert Bewerbungen für Kirchenämter
```javascript
{
id: INTEGER (PK, auto-increment)
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
characterId: INTEGER (FK -> FalukantCharacter.id)
regionId: INTEGER (FK -> RegionData.id)
supervisorId: INTEGER (FK -> FalukantCharacter.id)
status: ENUM('pending', 'approved', 'rejected')
decisionDate: DATE (nullable)
createdAt: DATE
updatedAt: DATE
}
```
**Beziehungen:**
- `belongsTo` ChurchOfficeType (als `officeType`)
- `belongsTo` FalukantCharacter (als `applicant`)
- `belongsTo` FalukantCharacter (als `supervisor`)
- `belongsTo` RegionData (als `region`)
---
## Zusätzlich benötigte Models (für Daemon)
### RegionData (falukant_data.region)
- Wird für `regionId` in ChurchOffice und ChurchApplication benötigt
- Enthält `regionType` (country, duchy, markgravate, shire, county, city)
- Enthält `parentId` für Hierarchie
### FalukantCharacter (falukant_data.character)
- Wird für `characterId` (Inhaber/Bewerber) benötigt
- Wird für `supervisorId` benötigt
---
## Wichtige Queries für Daemon
### Verfügbare Positionen finden
```sql
SELECT cot.*, COUNT(co.id) as occupied_seats
FROM falukant_type.church_office_type cot
LEFT JOIN falukant_data.church_office co
ON cot.id = co.office_type_id
AND co.region_id = ?
WHERE cot.region_type = ?
GROUP BY cot.id
HAVING COUNT(co.id) < cot.seats_per_region
```
### Supervisor finden
```sql
SELECT co.*
FROM falukant_data.church_office co
JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id
WHERE co.region_id = ?
AND cot.hierarchy_level > (
SELECT hierarchy_level
FROM falukant_type.church_office_type
WHERE id = ?
)
ORDER BY cot.hierarchy_level ASC
LIMIT 1
```
### Voraussetzungen prüfen
```sql
SELECT cor.*
FROM falukant_predefine.church_office_requirement cor
WHERE cor.office_type_id = ?
```
### Bewerbungen für Supervisor
```sql
SELECT ca.*
FROM falukant_data.church_application ca
WHERE ca.supervisor_id = ?
AND ca.status = 'pending'
```

78
CHURCH_OFFICES.md Normal file
View File

@@ -0,0 +1,78 @@
# Kirchenämter - Hierarchie und Verfügbarkeit
## Regionstypen
- **country** (Land): Falukant
- **duchy** (Herzogtum): Hessen
- **markgravate** (Markgrafschaft): Groß-Benbach
- **shire** (Grafschaft): Siebenbachen
- **county** (Kreis): Bad Homburg, Maintal
- **city** (Stadt): Frankfurt, Oberursel, Offenbach, Königstein
## Kirchenämter (von höchstem zu niedrigstem Rang)
| Amt | Translation Key | Hierarchie-Level | Regionstyp | Plätze pro Region | Beschreibung |
|-----|----------------|-------------------|------------|-------------------|--------------|
| **Papst** | `pope` | 8 | country | 1 | Höchstes Amt, nur einer im ganzen Land |
| **Kardinal** | `cardinal` | 7 | country | 3 | Höchste Kardinäle, mehrere pro Land möglich |
| **Erzbischof** | `archbishop` | 6 | duchy | 1 | Pro Herzogtum ein Erzbischof |
| **Bischof** | `bishop` | 5 | markgravate | 1 | Pro Markgrafschaft ein Bischof |
| **Erzdiakon** | `archdeacon` | 4 | shire | 1 | Pro Grafschaft ein Erzdiakon |
| **Dekan** | `dean` | 3 | county | 1 | Pro Kreis ein Dekan |
| **Pfarrer** | `parish-priest` | 2 | city | 1 | Pro Stadt ein Pfarrer |
| **Dorfgeistlicher** | `village-priest` | 1 | city | 1 | Pro Stadt ein Dorfgeistlicher (Einstiegsposition) |
| **Laienprediger** | `lay-preacher` | 0 | city | 3 | Pro Stadt mehrere Laienprediger (niedrigste Position) |
## Verfügbare Positionen pro Regionstyp
### country (Land: Falukant)
- **Papst**: 1 Platz
- **Kardinal**: 3 Plätze
- **Gesamt**: 4 Plätze
### duchy (Herzogtum: Hessen)
- **Erzbischof**: 1 Platz
- **Gesamt**: 1 Platz
### markgravate (Markgrafschaft: Groß-Benbach)
- **Bischof**: 1 Platz
- **Gesamt**: 1 Platz
### shire (Grafschaft: Siebenbachen)
- **Erzdiakon**: 1 Platz
- **Gesamt**: 1 Platz
### county (Kreis: Bad Homburg, Maintal)
- **Dekan**: 1 Platz pro Kreis
- **Gesamt**: 1 Platz pro Kreis
### city (Stadt: Frankfurt, Oberursel, Offenbach, Königstein)
- **Pfarrer**: 1 Platz pro Stadt
- **Dorfgeistlicher**: 1 Platz pro Stadt
- **Laienprediger**: 3 Plätze pro Stadt
- **Gesamt**: 5 Plätze pro Stadt
## Hierarchie und Beförderungsweg
1. **Laienprediger** (lay-preacher) - Einstiegsposition, keine Voraussetzung
2. **Dorfgeistlicher** (village-priest) - Voraussetzung: Laienprediger
3. **Pfarrer** (parish-priest) - Voraussetzung: Dorfgeistlicher
4. **Dekan** (dean) - Voraussetzung: Pfarrer
5. **Erzdiakon** (archdeacon) - Voraussetzung: Dekan
6. **Bischof** (bishop) - Voraussetzung: Erzdiakon
7. **Erzbischof** (archbishop) - Voraussetzung: Bischof
8. **Kardinal** (cardinal) - Voraussetzung: Erzbischof
9. **Papst** (pope) - Voraussetzung: Kardinal
## Gesamtübersicht verfügbarer Positionen
- **Papst**: 1 Position (Land)
- **Kardinal**: 3 Positionen (Land)
- **Erzbischof**: 1 Position (Herzogtum)
- **Bischof**: 1 Position (Markgrafschaft)
- **Erzdiakon**: 1 Position (Grafschaft)
- **Dekan**: 2 Positionen (2 Kreise)
- **Pfarrer**: 4 Positionen (4 Städte)
- **Dorfgeistlicher**: 4 Positionen (4 Städte)
- **Laienprediger**: 12 Positionen (4 Städte × 3)
**Gesamt**: 30 Positionen im System

119
CMakeLists.txt Normal file
View File

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

414
CMakeLists.txt.user Normal file
View File

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

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

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

168
SSL-SETUP.md Normal file
View File

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

184
backend/analyze-indexes.js Executable file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env node
/**
* Script zur Analyse und Empfehlung von Indizes
*
* Analysiert:
* - Tabellen mit vielen Sequential Scans
* - Fehlende Composite Indizes für häufige JOINs
* - Ungenutzte Indizes
*/
import './config/loadEnv.js';
import { sequelize } from './utils/sequelize.js';
async function main() {
try {
console.log('🔍 Index-Analyse und Empfehlungen\n');
console.log('='.repeat(60) + '\n');
// 1. Tabellen mit vielen Sequential Scans
await analyzeSequentialScans();
// 2. Prüfe häufige JOIN-Patterns
await analyzeJoinPatterns();
// 3. Ungenutzte Indizes
await analyzeUnusedIndexes();
console.log('='.repeat(60));
console.log('✅ Analyse abgeschlossen\n');
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('❌ Fehler:', error.message);
console.error(error.stack);
process.exit(1);
}
}
async function analyzeSequentialScans() {
console.log('📊 1. Tabellen mit vielen Sequential Scans\n');
const [tables] = await sequelize.query(`
SELECT
schemaname || '.' || relname as table_name,
seq_scan,
seq_tup_read,
idx_scan,
seq_tup_read / NULLIF(seq_scan, 0) as avg_rows_per_scan,
CASE
WHEN seq_scan + idx_scan > 0
THEN round((seq_scan::numeric / (seq_scan + idx_scan)) * 100, 2)
ELSE 0
END as seq_scan_percent
FROM pg_stat_user_tables
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
AND seq_scan > 1000
ORDER BY seq_tup_read DESC
LIMIT 10;
`);
if (tables.length > 0) {
console.log(' ⚠️ Tabellen mit vielen Sequential Scans:');
tables.forEach(t => {
console.log(`\n ${t.table_name}:`);
console.log(` Sequential Scans: ${parseInt(t.seq_scan).toLocaleString()}`);
console.log(` Zeilen gelesen: ${parseInt(t.seq_tup_read).toLocaleString()}`);
console.log(` Index Scans: ${parseInt(t.idx_scan).toLocaleString()}`);
console.log(` Seq Scan Anteil: ${t.seq_scan_percent}%`);
console.log(` Ø Zeilen pro Scan: ${parseInt(t.avg_rows_per_scan).toLocaleString()}`);
if (t.seq_scan_percent > 50) {
console.log(` ⚠️ KRITISCH: Mehr als 50% Sequential Scans!`);
}
});
console.log('');
}
}
async function analyzeJoinPatterns() {
console.log('🔗 2. Analyse häufiger JOIN-Patterns\n');
// Prüfe welche Indizes auf knowledge existieren
const [knowledgeIndexes] = await sequelize.query(`
SELECT
indexname,
indexdef
FROM pg_indexes
WHERE schemaname = 'falukant_data'
AND tablename = 'knowledge'
ORDER BY indexname;
`);
console.log(' Indizes auf falukant_data.knowledge:');
if (knowledgeIndexes.length > 0) {
knowledgeIndexes.forEach(idx => {
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
});
} else {
console.log(' Keine Indizes gefunden');
}
console.log('');
// Empfehlung: Composite Index auf (character_id, product_id)
const [knowledgeUsage] = await sequelize.query(`
SELECT
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE schemaname = 'falukant_data'
AND relname = 'knowledge'
AND indexrelname = 'idx_knowledge_character_id';
`);
if (knowledgeUsage.length > 0) {
const usage = knowledgeUsage[0];
console.log(' Aktuelle Nutzung von idx_knowledge_character_id:');
console.log(` Scans: ${parseInt(usage.idx_scan).toLocaleString()}`);
console.log(` Zeilen gelesen: ${parseInt(usage.idx_tup_read).toLocaleString()}`);
console.log('');
console.log(' 💡 Empfehlung:');
console.log(' CREATE INDEX IF NOT EXISTS idx_knowledge_character_product');
console.log(' ON falukant_data.knowledge(character_id, product_id);');
console.log(' → Wird häufig für JOINs mit character_id UND product_id verwendet\n');
}
// Prüfe character Indizes
const [characterIndexes] = await sequelize.query(`
SELECT
indexname,
indexdef
FROM pg_indexes
WHERE schemaname = 'falukant_data'
AND tablename = 'character'
ORDER BY indexname;
`);
console.log(' Indizes auf falukant_data.character:');
if (characterIndexes.length > 0) {
characterIndexes.forEach(idx => {
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
});
}
console.log('');
}
async function analyzeUnusedIndexes() {
console.log('🗑️ 3. Ungenutzte Indizes\n');
const [unused] = await sequelize.query(`
SELECT
schemaname || '.' || indexrelname as index_name,
schemaname || '.' || relname as table_name,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
idx_scan as scans,
pg_relation_size(indexrelid) as size_bytes
FROM pg_stat_user_indexes
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
AND idx_scan = 0
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
`);
if (unused.length > 0) {
console.log(' ⚠️ Ungenutzte Indizes (> 1MB):');
unused.forEach(idx => {
console.log(` ${idx.index_name} auf ${idx.table_name}`);
console.log(` Größe: ${idx.index_size}, Scans: ${idx.scans}`);
});
console.log('');
console.log(' 💡 Überlege, ob diese Indizes gelöscht werden können:');
console.log(' DROP INDEX IF EXISTS <index_name>;');
console.log('');
} else {
console.log(' ✅ Keine großen ungenutzten Indizes gefunden\n');
}
}
main();

View File

@@ -12,6 +12,7 @@ import socialnetworkRouter from './routers/socialnetworkRouter.js';
import forumRouter from './routers/forumRouter.js'; import forumRouter from './routers/forumRouter.js';
import falukantRouter from './routers/falukantRouter.js'; import falukantRouter from './routers/falukantRouter.js';
import friendshipRouter from './routers/friendshipRouter.js'; import friendshipRouter from './routers/friendshipRouter.js';
import modelsProxyRouter from './routers/modelsProxyRouter.js';
import blogRouter from './routers/blogRouter.js'; import blogRouter from './routers/blogRouter.js';
import match3Router from './routers/match3Router.js'; import match3Router from './routers/match3Router.js';
import taxiRouter from './routers/taxiRouter.js'; import taxiRouter from './routers/taxiRouter.js';
@@ -19,6 +20,9 @@ import taxiMapRouter from './routers/taxiMapRouter.js';
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js'; import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
import termineRouter from './routers/termineRouter.js'; import termineRouter from './routers/termineRouter.js';
import vocabRouter from './routers/vocabRouter.js'; import vocabRouter from './routers/vocabRouter.js';
import dashboardRouter from './routers/dashboardRouter.js';
import newsRouter from './routers/newsRouter.js';
import calendarRouter from './routers/calendarRouter.js';
import cors from 'cors'; import cors from 'cors';
import './jobs/sessionCleanup.js'; import './jobs/sessionCleanup.js';
@@ -74,11 +78,22 @@ app.use('/api/vocab', vocabRouter);
app.use('/api/forum', forumRouter); app.use('/api/forum', forumRouter);
app.use('/api/falukant', falukantRouter); app.use('/api/falukant', falukantRouter);
app.use('/api/friendships', friendshipRouter); app.use('/api/friendships', friendshipRouter);
app.use('/api/models', modelsProxyRouter);
app.use('/api/blog', blogRouter); app.use('/api/blog', blogRouter);
app.use('/api/termine', termineRouter); app.use('/api/termine', termineRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/news', newsRouter);
app.use('/api/calendar', calendarRouter);
// Serve frontend SPA for non-API routes to support history mode clean URLs // Serve frontend SPA for non-API routes to support history mode clean URLs
// /models/* nicht statisch ausliefern nur über /api/models (Proxy mit Komprimierung)
const frontendDir = path.join(__dirname, '../frontend'); const frontendDir = path.join(__dirname, '../frontend');
app.use((req, res, next) => {
if (req.path.startsWith('/models/')) {
return res.status(404).send('Use /api/models/ for 3D models (optimized).');
}
next();
});
app.use(express.static(path.join(frontendDir, 'dist'))); app.use(express.static(path.join(frontendDir, 'dist')));
app.get(/^\/(?!api\/).*/, (req, res) => { app.get(/^\/(?!api\/).*/, (req, res) => {
res.sendFile(path.join(frontendDir, 'dist', 'index.html')); res.sendFile(path.join(frontendDir, 'dist', 'index.html'));

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* Script zum Prüfen und Bereinigen von PostgreSQL-Verbindungen
*/
import './config/loadEnv.js';
import { sequelize } from './utils/sequelize.js';
async function main() {
try {
console.log('🔍 Prüfe PostgreSQL-Verbindungen...\n');
// Prüfe aktive Verbindungen
const [connections] = await sequelize.query(`
SELECT
count(*) as total,
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle,
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
count(*) FILTER (WHERE usename = current_user) as my_connections
FROM pg_stat_activity
WHERE datname = current_database();
`);
console.log('📊 Verbindungsstatistik:');
console.log(` Gesamt: ${connections[0].total}`);
console.log(` Aktiv: ${connections[0].active}`);
console.log(` Idle: ${connections[0].idle}`);
console.log(` Idle in Transaction: ${connections[0].idle_in_transaction}`);
console.log(` Meine Verbindungen: ${connections[0].my_connections}\n`);
// Prüfe max_connections Limit
const [maxConn] = await sequelize.query(`
SELECT setting::int as max_connections
FROM pg_settings
WHERE name = 'max_connections';
`);
console.log(`📈 Max Connections Limit: ${maxConn[0].max_connections}`);
console.log(`📉 Verfügbare Connections: ${maxConn[0].max_connections - connections[0].total}\n`);
// Zeige alte idle Verbindungen
const [oldConnections] = await sequelize.query(`
SELECT
pid,
usename,
application_name,
state,
state_change,
now() - state_change as idle_duration,
query
FROM pg_stat_activity
WHERE datname = current_database()
AND state = 'idle'
AND state_change < now() - interval '1 minute'
ORDER BY state_change ASC
LIMIT 10;
`);
if (oldConnections.length > 0) {
console.log(`⚠️ Gefunden ${oldConnections.length} alte idle Verbindungen (> 1 Minute):`);
oldConnections.forEach(conn => {
console.log(` PID: ${conn.pid}, User: ${conn.usename}, Idle seit: ${conn.idle_duration}`);
});
console.log('\n💡 Tipp: Du kannst alte Verbindungen beenden mit:');
console.log(' SELECT pg_terminate_backend(pid) FROM pg_stat_activity');
console.log(' WHERE datname = current_database() AND state = \'idle\' AND state_change < now() - interval \'5 minutes\';\n');
}
// Prüfe ob wir nahe am Limit sind
const usagePercent = (connections[0].total / maxConn[0].max_connections) * 100;
if (usagePercent > 80) {
console.log(`⚠️ WARNUNG: ${usagePercent.toFixed(1)}% der verfügbaren Verbindungen werden verwendet!`);
console.log(' Es könnte sein, dass nicht genug Verbindungen verfügbar sind.\n');
}
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('❌ Fehler:', error.message);
process.exit(1);
}
}
main();

142
backend/check-knowledge-pkey.js Executable file
View File

@@ -0,0 +1,142 @@
#!/usr/bin/env node
/**
* Script zur Analyse des knowledge_pkey Problems
*
* Prüft warum knowledge_pkey nicht verwendet wird
*/
import './config/loadEnv.js';
import { sequelize } from './utils/sequelize.js';
async function main() {
try {
console.log('🔍 Analyse knowledge_pkey Problem\n');
console.log('='.repeat(60) + '\n');
// Prüfe ob knowledge einen Primary Key hat
const [pkInfo] = await sequelize.query(`
SELECT
a.attname as column_name,
t.conname as constraint_name,
t.contype as constraint_type
FROM pg_constraint t
JOIN pg_class c ON c.oid = t.conrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(t.conkey)
WHERE n.nspname = 'falukant_data'
AND c.relname = 'knowledge'
AND t.contype = 'p';
`);
console.log('📋 Primary Key Information:');
if (pkInfo.length > 0) {
pkInfo.forEach(pk => {
console.log(` Constraint: ${pk.constraint_name}`);
console.log(` Spalte: ${pk.column_name}`);
console.log(` Typ: ${pk.constraint_type === 'p' ? 'PRIMARY KEY' : pk.constraint_type}`);
});
} else {
console.log(' ⚠️ Kein Primary Key gefunden!');
}
console.log('');
// Prüfe alle Indizes auf knowledge
const [allIndexes] = await sequelize.query(`
SELECT
indexname,
indexdef,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_indexes
LEFT JOIN pg_stat_user_indexes
ON pg_stat_user_indexes.indexrelname = pg_indexes.indexname
AND pg_stat_user_indexes.schemaname = pg_indexes.schemaname
WHERE pg_indexes.schemaname = 'falukant_data'
AND pg_indexes.tablename = 'knowledge'
ORDER BY indexname;
`);
console.log('📊 Alle Indizes auf knowledge:');
allIndexes.forEach(idx => {
console.log(`\n ${idx.indexname}:`);
console.log(` Definition: ${idx.indexdef}`);
console.log(` Scans: ${idx.idx_scan ? parseInt(idx.idx_scan).toLocaleString() : 'N/A'}`);
console.log(` Zeilen gelesen: ${idx.idx_tup_read ? parseInt(idx.idx_tup_read).toLocaleString() : 'N/A'}`);
});
console.log('');
// Prüfe Tabellenstruktur
const [tableStructure] = await sequelize.query(`
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'knowledge'
ORDER BY ordinal_position;
`);
console.log('📋 Tabellenstruktur:');
tableStructure.forEach(col => {
console.log(` ${col.column_name}: ${col.data_type} ${col.is_nullable === 'NO' ? 'NOT NULL' : 'NULL'}`);
});
console.log('');
// Erklärung: Warum knowledge_pkey ungenutzt ist
const pkUnused = allIndexes.find(i => i.indexname === 'knowledge_pkey' && (i.idx_scan == null || parseInt(i.idx_scan) === 0));
if (pkUnused) {
console.log('💡 Warum knowledge_pkey (0 Scans) ungenutzt ist:');
console.log(' Alle Zugriffe filtern nach (character_id, product_id), nie nach id.');
console.log(' Der PK-Index wird nur für Eindeutigkeit/Referenzen genutzt, nicht für Lookups.');
console.log(' idx_knowledge_character_product deckt die tatsächlichen Queries ab.\n');
}
// Prüfe ob Queries mit id (Primary Key) gemacht werden
let idUsage = [];
try {
const [rows] = await sequelize.query(`
SELECT
query,
calls,
total_exec_time,
mean_exec_time
FROM pg_stat_statements
WHERE query LIKE '%knowledge%'
AND (query LIKE '%knowledge.id%' OR query LIKE '%knowledge%id%')
ORDER BY calls DESC
LIMIT 5;
`);
idUsage = rows;
} catch (e) {
console.log(' pg_stat_statements nicht verfügbar keine Query-Statistik.\n');
}
if (idUsage.length > 0) {
console.log('🔍 Queries die knowledge.id verwenden:');
idUsage.forEach(q => {
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}`);
console.log(` Query: ${q.query.substring(0, 150)}...`);
console.log('');
});
}
await sequelize.close();
process.exit(0);
} catch (error) {
if (error.message.includes('pg_stat_statements')) {
console.log(' ⚠️ pg_stat_statements ist nicht aktiviert oder nicht verfügbar\n');
} else {
console.error('❌ Fehler:', error.message);
console.error(error.stack);
}
await sequelize.close();
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* Script zum Bereinigen von alten/idle PostgreSQL-Verbindungen
*/
import './config/loadEnv.js';
import { sequelize } from './utils/sequelize.js';
async function main() {
try {
console.log('🧹 Bereinige alte PostgreSQL-Verbindungen...\n');
// Beende idle Verbindungen, die älter als 5 Minuten sind (außer unserer eigenen)
const [result] = await sequelize.query(`
SELECT pg_terminate_backend(pid) as terminated
FROM pg_stat_activity
WHERE datname = current_database()
AND pid <> pg_backend_pid()
AND state = 'idle'
AND state_change < now() - interval '5 minutes';
`);
const terminated = result.filter(r => r.terminated).length;
console.log(`${terminated} alte idle Verbindungen wurden beendet\n`);
// Zeige verbleibende Verbindungen
const [connections] = await sequelize.query(`
SELECT
count(*) as total,
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle
FROM pg_stat_activity
WHERE datname = current_database();
`);
console.log('📊 Verbleibende Verbindungen:');
console.log(` Gesamt: ${connections[0].total}`);
console.log(` Aktiv: ${connections[0].active}`);
console.log(` Idle: ${connections[0].idle}\n`);
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('❌ Fehler:', error.message);
if (error.message.includes('SUPERUSER')) {
console.error('\n💡 Tipp: Du benötigst Superuser-Rechte oder musst warten, bis Verbindungen freigegeben werden.');
console.error(' Versuche es in ein paar Minuten erneut.');
}
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,203 @@
import calendarService from '../services/calendarService.js';
function getHashedUserId(req) {
return req.headers?.userid;
}
export default {
/**
* GET /api/calendar/events
* Get all events for the authenticated user
* Query params: startDate, endDate (optional)
*/
async getEvents(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const { startDate, endDate } = req.query;
const events = await calendarService.getEvents(hashedUserId, { startDate, endDate });
res.json(events);
} catch (error) {
console.error('Calendar getEvents:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* GET /api/calendar/events/:id
* Get a single event by ID
*/
async getEvent(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const event = await calendarService.getEvent(hashedUserId, req.params.id);
res.json(event);
} catch (error) {
console.error('Calendar getEvent:', error);
if (error.message === 'Event not found') {
return res.status(404).json({ error: 'Event not found' });
}
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* POST /api/calendar/events
* Create a new event
*/
async createEvent(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const eventData = req.body;
if (!eventData.title || !eventData.startDate) {
return res.status(400).json({ error: 'Title and startDate are required' });
}
const event = await calendarService.createEvent(hashedUserId, eventData);
res.status(201).json(event);
} catch (error) {
console.error('Calendar createEvent:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* PUT /api/calendar/events/:id
* Update an existing event
*/
async updateEvent(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const eventData = req.body;
if (!eventData.title || !eventData.startDate) {
return res.status(400).json({ error: 'Title and startDate are required' });
}
const event = await calendarService.updateEvent(hashedUserId, req.params.id, eventData);
res.json(event);
} catch (error) {
console.error('Calendar updateEvent:', error);
if (error.message === 'Event not found') {
return res.status(404).json({ error: 'Event not found' });
}
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* DELETE /api/calendar/events/:id
* Delete an event
*/
async deleteEvent(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
await calendarService.deleteEvent(hashedUserId, req.params.id);
res.json({ success: true });
} catch (error) {
console.error('Calendar deleteEvent:', error);
if (error.message === 'Event not found') {
return res.status(404).json({ error: 'Event not found' });
}
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* GET /api/calendar/birthdays
* Get friends' birthdays for a given year
* Query params: year (required)
*/
async getFriendsBirthdays(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const year = parseInt(req.query.year) || new Date().getFullYear();
const birthdays = await calendarService.getFriendsBirthdays(hashedUserId, year);
res.json(birthdays);
} catch (error) {
console.error('Calendar getFriendsBirthdays:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* GET /api/calendar/widget/birthdays
* Get upcoming birthdays for widget display
*/
async getWidgetBirthdays(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const limit = parseInt(req.query.limit) || 10;
const birthdays = await calendarService.getUpcomingBirthdays(hashedUserId, limit);
res.json(birthdays);
} catch (error) {
console.error('Calendar getWidgetBirthdays:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* GET /api/calendar/widget/upcoming
* Get upcoming events for widget display
*/
async getWidgetUpcoming(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const limit = parseInt(req.query.limit) || 10;
const events = await calendarService.getUpcomingEvents(hashedUserId, limit);
res.json(events);
} catch (error) {
console.error('Calendar getWidgetUpcoming:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
/**
* GET /api/calendar/widget/mini
* Get mini calendar data for widget display
*/
async getWidgetMiniCalendar(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const data = await calendarService.getMiniCalendarData(hashedUserId);
res.json(data);
} catch (error) {
console.error('Calendar getWidgetMiniCalendar:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}
};

View File

@@ -0,0 +1,50 @@
import dashboardService from '../services/dashboardService.js';
function getHashedUserId(req) {
return req.headers?.userid;
}
export default {
/** Liste der möglichen Widget-Typen (öffentlich, keine Auth nötig wenn gewünscht aktuell mit Auth). */
async getAvailableWidgets(req, res) {
try {
const list = await dashboardService.getAvailableWidgets();
res.json(list);
} catch (error) {
console.error('Dashboard getAvailableWidgets:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
async getConfig(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const config = await dashboardService.getConfig(hashedUserId);
res.json(config);
} catch (error) {
console.error('Dashboard getConfig:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
async setConfig(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const config = req.body;
if (!config || typeof config !== 'object') {
return res.status(400).json({ error: 'Invalid config' });
}
try {
const result = await dashboardService.setConfig(hashedUserId, config);
res.json(result);
} catch (error) {
console.error('Dashboard setConfig:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}
};

View File

@@ -56,6 +56,10 @@ class FalukantController {
if (!page) page = 1; if (!page) page = 1;
return this.service.moneyHistory(userId, page, filter); return this.service.moneyHistory(userId, page, filter);
}); });
this.moneyHistoryGraph = this._wrapWithUser((userId, req) => {
const { range } = req.body || {};
return this.service.moneyHistoryGraph(userId, range || '24h');
});
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId)); this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
this.buyStorage = this._wrapWithUser((userId, req) => { this.buyStorage = this._wrapWithUser((userId, req) => {
const { branchId, amount, stockTypeId } = req.body; const { branchId, amount, stockTypeId } = req.body;
@@ -93,17 +97,35 @@ class FalukantController {
return result; return result;
}); });
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId)); this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId)); this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.cancelWooing = this._wrapWithUser(async (userId) => {
try {
return await this.service.cancelWooing(userId);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'cancelTooSoon') {
throw { status: 412, message: 'cancelTooSoon', retryAt: e.meta?.retryAt };
}
throw e;
}
}, { successStatus: 202 });
this.getGifts = this._wrapWithUser((userId) => { this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId); console.log('🔍 getGifts called with userId:', userId);
return this.service.getGifts(userId); return this.service.getGifts(userId);
}); });
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId)); this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
this.sendGift = this._wrapWithUser((userId, req) => this.service.sendGift(userId, req.body.giftId)); this.sendGift = this._wrapWithUser(async (userId, req) => {
try {
return await this.service.sendGift(userId, req.body.giftId);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooOften') {
throw { status: 412, message: 'tooOften', retryAt: e.meta?.retryAt };
}
throw e;
}
});
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId)); this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId)); this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId)); this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId)); this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
@@ -118,17 +140,22 @@ class FalukantController {
}, { successStatus: 201 }); }, { successStatus: 201 });
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId)); this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) => {
const { actionTypeId } = req.body;
return this.service.executeReputationAction(userId, actionTypeId);
}, { successStatus: 201 });
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId)); this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
this.baptise = this._wrapWithUser((userId, req) => { this.baptise = this._wrapWithUser((userId, req) => {
const { characterId: childId, firstName } = req.body; const { characterId: childId, firstName } = req.body;
return this.service.baptise(userId, childId, firstName); return this.service.baptise(userId, childId, firstName);
}); });
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
const { officeTypeId, regionId } = req.body;
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
});
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
const { applicationId, decision } = req.body;
return this.service.decideOnChurchApplication(userId, applicationId, decision);
});
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId)); this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
this.sendToSchool = this._wrapWithUser((userId, req) => { this.sendToSchool = this._wrapWithUser((userId, req) => {
@@ -144,7 +171,16 @@ class FalukantController {
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId)); this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId)); this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr)); this.healthActivity = this._wrapWithUser(async (userId, req) => {
try {
return await this.service.healthActivity(userId, req.body.measureTr);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
}
throw e;
}
});
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId)); this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId)); this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
@@ -162,17 +198,12 @@ class FalukantController {
} }
return this.service.getProductPriceInRegion(userId, productId, regionId); return this.service.getProductPriceInRegion(userId, productId, regionId);
}); });
this.getProductPricesInRegionBatch = this._wrapWithUser((userId, req) => { this.getAllProductPricesInRegion = this._wrapWithUser((userId, req) => {
const productIds = req.query.productIds;
const regionId = parseInt(req.query.regionId, 10); const regionId = parseInt(req.query.regionId, 10);
if (!productIds || Number.isNaN(regionId)) { if (Number.isNaN(regionId)) {
throw new Error('productIds (comma-separated) and regionId are required'); throw new Error('regionId is required');
} }
const productIdArray = productIds.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !Number.isNaN(id)); return this.service.getAllProductPricesInRegion(userId, regionId);
if (productIdArray.length === 0) {
throw new Error('At least one valid productId is required');
}
return this.service.getProductPricesInRegionBatch(userId, productIdArray, regionId);
}); });
this.getProductPricesInCities = this._wrapWithUser((userId, req) => { this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10); const productId = parseInt(req.query.productId, 10);
@@ -183,6 +214,16 @@ class FalukantController {
} }
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId); return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
}); });
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => {
const body = req.body || {};
const items = Array.isArray(body.items) ? body.items : [];
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
const valid = items.map(i => ({
productId: parseInt(i.productId, 10),
currentPrice: parseFloat(i.currentPrice)
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
});
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element)); this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId)); this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
@@ -190,6 +231,7 @@ class FalukantController {
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId)); this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size)); this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 }); this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId)); this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
this.searchUsers = this._wrapWithUser((userId, req) => { this.searchUsers = this._wrapWithUser((userId, req) => {
@@ -264,7 +306,13 @@ class FalukantController {
} catch (error) { } catch (error) {
console.error('Controller error:', error); console.error('Controller error:', error);
const status = error.status && typeof error.status === 'number' ? error.status : 500; const status = error.status && typeof error.status === 'number' ? error.status : 500;
res.status(status).json({ error: error.message || 'Internal error' }); // Wenn error ein Objekt mit status ist, alle Felder außer status übernehmen
if (error && typeof error === 'object' && error.status && typeof error.status === 'number') {
const { status: errorStatus, ...errorData } = error;
res.status(errorStatus).json({ error: error.message || errorData.message || 'Internal error', ...errorData });
} else {
res.status(status).json({ error: error.message || 'Internal error' });
}
} }
}; };
} }

View File

@@ -50,11 +50,6 @@ const menuStructure = {
visible: ["all"], visible: ["all"],
path: "/socialnetwork/gallery" path: "/socialnetwork/gallery"
}, },
vocabtrainer: {
visible: ["all"],
path: "/socialnetwork/vocab",
children: {}
},
blockedUsers: { blockedUsers: {
visible: ["all"], visible: ["all"],
path: "/socialnetwork/blocked" path: "/socialnetwork/blocked"
@@ -183,6 +178,30 @@ const menuStructure = {
} }
} }
}, },
personal: {
visible: ["all"],
icon: "profile16.png",
children: {
sprachenlernen: {
visible: ["all"],
children: {
vocabtrainer: {
visible: ["all"],
path: "/socialnetwork/vocab",
showVocabLanguages: 1 // Flag für dynamische Sprachen-Liste
},
sprachkurse: {
visible: ["all"],
path: "/socialnetwork/vocab/courses"
}
}
},
calendar: {
visible: ["all"],
path: "/personal/calendar"
}
}
},
settings: { settings: {
visible: ["all"], visible: ["all"],
icon: "settings16.png", icon: "settings16.png",
@@ -377,22 +396,9 @@ class NavigationController {
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean); const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id); const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
// Dynamisches Submenü: Treffpunkt → Vokabeltrainer → (Neue Sprache + abonnierte/angelegte) // Vokabeltrainer: Sprachen werden im Frontend dynamisch geladen (wie Forum)
// Wichtig: "Neue Sprache" soll IMMER sichtbar sein auch wenn die DB-Abfrage (noch) fehlschlägt. // Keine children mehr, da das Menü nur 2 Ebenen unterstützt
if (filteredMenu?.socialnetwork?.children?.vocabtrainer) { // Das Frontend lädt die Sprachen separat und zeigt sie als submenu2 an
const children = {
newLanguage: { path: '/socialnetwork/vocab/new' },
};
try {
const langs = await this.vocabService.listLanguagesForMenu(user.id);
for (const l of langs) {
children[`lang_${l.id}`] = { path: `/socialnetwork/vocab/${l.id}`, label: l.name };
}
} catch (e) {
console.warn('[menu] Konnte Vokabeltrainer-Sprachen nicht laden:', e?.message || e);
}
filteredMenu.socialnetwork.children.vocabtrainer.children = children;
}
res.status(200).json(filteredMenu); res.status(200).json(filteredMenu);
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,21 @@
import newsService from '../services/newsService.js';
/**
* GET /api/news?counter=0&language=de&category=top
* counter = wievieltes News-Widget aufgerufen wird (0, 1, 2, …), damit keine doppelten Artikel.
*/
export default {
async getNews(req, res) {
const counter = Math.max(0, parseInt(req.query.counter, 10) || 0);
const language = (req.query.language || 'de').slice(0, 10);
const category = (req.query.category || 'top').slice(0, 50);
try {
const { results, nextPage } = await newsService.getNews({ counter, language, category });
res.json({ results, nextPage });
} catch (error) {
console.error('News getNews:', error);
res.status(500).json({ error: error.message || 'News konnten nicht geladen werden.' });
}
}
};

View File

@@ -9,6 +9,7 @@ class VocabController {
this.service = new VocabService(); this.service = new VocabService();
this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId)); this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId));
this.listAllLanguages = this._wrapWithUser(() => this.service.listAllLanguages());
this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 }); this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 });
this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 }); this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 });
this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId)); this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId));
@@ -21,6 +22,39 @@ class VocabController {
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId)); this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId)); this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 }); this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
// Courses
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 });
this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query));
this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
// Lessons
this.getLesson = this._wrapWithUser((userId, req) => this.service.getLesson(userId, req.params.lessonId));
this.addLessonToCourse = this._wrapWithUser((userId, req) => this.service.addLessonToCourse(userId, req.params.courseId, req.body), { successStatus: 201 });
this.updateLesson = this._wrapWithUser((userId, req) => this.service.updateLesson(userId, req.params.lessonId, req.body));
this.deleteLesson = this._wrapWithUser((userId, req) => this.service.deleteLesson(userId, req.params.lessonId));
// Enrollment
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 });
this.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
// Progress
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId));
this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
// Grammar Exercises
this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes());
this.createGrammarExercise = this._wrapWithUser((userId, req) => this.service.createGrammarExercise(userId, req.params.lessonId, req.body), { successStatus: 201 });
this.getGrammarExercisesForLesson = this._wrapWithUser((userId, req) => this.service.getGrammarExercisesForLesson(userId, req.params.lessonId));
this.getGrammarExercise = this._wrapWithUser((userId, req) => this.service.getGrammarExercise(userId, req.params.exerciseId));
this.checkGrammarExerciseAnswer = this._wrapWithUser((userId, req) => this.service.checkGrammarExerciseAnswer(userId, req.params.exerciseId, req.body.answer));
this.getGrammarExerciseProgress = this._wrapWithUser((userId, req) => this.service.getGrammarExerciseProgress(userId, req.params.lessonId));
this.updateGrammarExercise = this._wrapWithUser((userId, req) => this.service.updateGrammarExercise(userId, req.params.exerciseId, req.body));
this.deleteGrammarExercise = this._wrapWithUser((userId, req) => this.service.deleteGrammarExercise(userId, req.params.exerciseId));
} }
_wrapWithUser(fn, { successStatus = 200 } = {}) { _wrapWithUser(fn, { successStatus = 200 } = {}) {

View File

@@ -0,0 +1,159 @@
#!/usr/bin/env node
/**
* Script zum Erstellen von Performance-Indizes
*
* Erstellt Indizes basierend auf der Analyse häufiger Queries:
* - inventory: stock_id
* - stock: branch_id
* - production: branch_id
* - director: employer_user_id
* - knowledge: (character_id, product_id) composite
*/
import './config/loadEnv.js';
import { sequelize } from './utils/sequelize.js';
async function main() {
try {
console.log('🔧 Erstelle Performance-Indizes\n');
console.log('='.repeat(60) + '\n');
const indexes = [
{
name: 'idx_knowledge_character_product',
table: 'falukant_data.knowledge',
columns: '(character_id, product_id)',
description: 'Composite Index für JOINs mit character_id UND product_id',
critical: true
},
{
name: 'idx_inventory_stock_id',
table: 'falukant_data.inventory',
columns: '(stock_id)',
description: 'Index für WHERE inventory.stock_id = ...',
critical: true
},
{
name: 'idx_stock_branch_id',
table: 'falukant_data.stock',
columns: '(branch_id)',
description: 'Index für WHERE stock.branch_id = ...',
critical: true
},
{
name: 'idx_production_branch_id',
table: 'falukant_data.production',
columns: '(branch_id)',
description: 'Index für WHERE production.branch_id = ...',
critical: true
},
{
name: 'idx_director_employer_user_id',
table: 'falukant_data.director',
columns: '(employer_user_id)',
description: 'Index für WHERE director.employer_user_id = ...',
critical: true
},
{
name: 'idx_production_start_timestamp',
table: 'falukant_data.production',
columns: '(start_timestamp)',
description: 'Index für WHERE production.start_timestamp <= ...',
critical: false
},
{
name: 'idx_director_last_salary_payout',
table: 'falukant_data.director',
columns: '(last_salary_payout)',
description: 'Index für WHERE director.last_salary_payout < ...',
critical: false
}
];
console.log(`📋 ${indexes.length} Indizes werden erstellt:\n`);
let created = 0;
let skipped = 0;
let errors = 0;
for (let i = 0; i < indexes.length; i++) {
const idx = indexes[i];
const criticalMark = idx.critical ? ' ⚠️ KRITISCH' : '';
console.log(`[${i + 1}/${indexes.length}] ${idx.name}${criticalMark}`);
console.log(` Tabelle: ${idx.table}`);
console.log(` Spalten: ${idx.columns}`);
console.log(` Beschreibung: ${idx.description}`);
try {
// Prüfe ob Index bereits existiert
const [existing] = await sequelize.query(`
SELECT EXISTS(
SELECT 1 FROM pg_indexes
WHERE schemaname || '.' || tablename = '${idx.table}'
AND indexname = '${idx.name}'
) as exists;
`);
if (existing[0].exists) {
console.log(` ⏭️ Index existiert bereits, überspringe\n`);
skipped++;
continue;
}
// Erstelle Index
const startTime = Date.now();
await sequelize.query(`
CREATE INDEX IF NOT EXISTS ${idx.name}
ON ${idx.table} USING btree ${idx.columns};
`);
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(` ✅ Erstellt in ${duration}s\n`);
created++;
} catch (error) {
console.error(` ❌ Fehler: ${error.message}\n`);
errors++;
}
}
console.log('='.repeat(60));
console.log(`✅ Zusammenfassung:`);
console.log(` Erstellt: ${created}`);
console.log(` Übersprungen: ${skipped}`);
console.log(` Fehler: ${errors}\n`);
// ANALYZE ausführen, damit PostgreSQL die neuen Indizes berücksichtigt
const tablesToAnalyze = [
'falukant_data.knowledge',
'falukant_data.inventory',
'falukant_data.stock',
'falukant_data.production',
'falukant_data.director'
];
if (created > 0) {
console.log('📊 Führe ANALYZE auf betroffenen Tabellen aus...\n');
for (const table of tablesToAnalyze) {
try {
await sequelize.query(`ANALYZE ${table};`);
console.log(` ✅ ANALYZE ${table};`);
} catch (err) {
console.log(` ⚠️ ${table}: ${err.message}`);
}
}
console.log('');
}
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('❌ Fehler:', error.message);
console.error(error.stack);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,479 @@
#!/usr/bin/env node
/**
* Umfassendes Diagnose-Script für Datenbank-Performance
*
* Untersucht:
* - Verbindungsstatistiken
* - Langsame Queries
* - Tabellengrößen und Bloat
* - Indizes (fehlende/ungenutzte)
* - Vacuum/Analyze Status
* - Locking/Blocking
* - Query-Statistiken
*/
import './config/loadEnv.js';
import { sequelize } from './utils/sequelize.js';
async function main() {
try {
console.log('🔍 Datenbank-Performance-Diagnose\n');
console.log('='.repeat(60) + '\n');
// 1. Verbindungsstatistiken
await checkConnections();
// 2. Langsame Queries (wenn pg_stat_statements aktiviert ist)
await checkSlowQueries();
// 3. Tabellengrößen und Bloat
await checkTableSizes();
// 4. Indizes prüfen
await checkIndexes();
// 5. Vacuum/Analyze Status
await checkVacuumStatus();
// 6. Locking/Blocking
await checkLocks();
// 7. Query-Statistiken (wenn pg_stat_statements aktiviert ist)
await checkQueryStats();
// 8. Connection Pool Status
await checkConnectionPool();
console.log('\n' + '='.repeat(60));
console.log('✅ Diagnose abgeschlossen\n');
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('❌ Fehler:', error.message);
console.error(error.stack);
process.exit(1);
}
}
async function checkConnections() {
console.log('📊 1. Verbindungsstatistiken\n');
const [connections] = await sequelize.query(`
SELECT
count(*) as total,
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle,
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
count(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting
FROM pg_stat_activity
WHERE datname = current_database();
`);
const conn = connections[0];
console.log(` Gesamt: ${conn.total}`);
console.log(` Aktiv: ${conn.active}`);
console.log(` Idle: ${conn.idle}`);
console.log(` Idle in Transaction: ${conn.idle_in_transaction}`);
console.log(` Wartend: ${conn.waiting}\n`);
const [maxConn] = await sequelize.query(`
SELECT setting::int as max_connections
FROM pg_settings
WHERE name = 'max_connections';
`);
const usagePercent = (conn.total / maxConn[0].max_connections) * 100;
console.log(` Max Connections: ${maxConn[0].max_connections}`);
console.log(` Auslastung: ${usagePercent.toFixed(1)}%\n`);
if (usagePercent > 80) {
console.log(' ⚠️ WARNUNG: Hohe Verbindungsauslastung!\n');
}
// Zeige lange laufende Queries
const [longRunning] = await sequelize.query(`
SELECT
pid,
usename,
application_name,
state,
now() - query_start as duration,
wait_event_type,
wait_event,
left(query, 100) as query_preview
FROM pg_stat_activity
WHERE datname = current_database()
AND state != 'idle'
AND now() - query_start > interval '5 seconds'
ORDER BY query_start ASC
LIMIT 10;
`);
if (longRunning.length > 0) {
console.log(' ⚠️ Lange laufende Queries (> 5 Sekunden):');
longRunning.forEach(q => {
const duration = Math.round(q.duration.total_seconds);
console.log(` PID ${q.pid}: ${duration}s - ${q.query_preview}...`);
});
console.log('');
}
}
async function checkSlowQueries() {
console.log('🐌 2. Langsame Queries (pg_stat_statements)\n');
try {
// Prüfe ob pg_stat_statements aktiviert ist
const [extension] = await sequelize.query(`
SELECT EXISTS(
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
) as exists;
`);
if (!extension[0].exists) {
console.log(' pg_stat_statements ist nicht aktiviert.');
console.log(' 💡 Aktivieren mit: CREATE EXTENSION IF NOT EXISTS pg_stat_statements;\n');
return;
}
const [slowQueries] = await sequelize.query(`
SELECT
left(query, 100) as query_preview,
calls,
total_exec_time,
mean_exec_time,
max_exec_time,
(total_exec_time / sum(total_exec_time) OVER ()) * 100 as percent_total
FROM pg_stat_statements
WHERE mean_exec_time > 100 -- Queries mit > 100ms Durchschnitt
ORDER BY total_exec_time DESC
LIMIT 10;
`);
if (slowQueries.length > 0) {
console.log(' Top 10 langsamste Queries (nach Gesamtzeit):');
slowQueries.forEach((q, i) => {
console.log(` ${i + 1}. ${q.query_preview}...`);
console.log(` Aufrufe: ${q.calls}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms, Max: ${q.max_exec_time.toFixed(2)}ms`);
console.log(` Gesamtzeit: ${q.total_exec_time.toFixed(2)}ms (${q.percent_total.toFixed(1)}%)\n`);
});
} else {
console.log(' ✅ Keine sehr langsamen Queries gefunden (> 100ms Durchschnitt)\n');
}
} catch (error) {
console.log(` ⚠️ Fehler beim Abrufen der Query-Statistiken: ${error.message}\n`);
}
}
async function checkTableSizes() {
console.log('📦 3. Tabellengrößen und Bloat\n');
const [tableSizes] = await sequelize.query(`
SELECT
schemaname || '.' || relname as full_table_name,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size,
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) as table_size,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) as indexes_size,
n_live_tup as row_count,
n_dead_tup as dead_rows,
CASE
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
ELSE 0
END as dead_row_percent,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC
LIMIT 20;
`);
if (tableSizes.length > 0) {
console.log(' Top 20 größte Tabellen:');
tableSizes.forEach((t, i) => {
console.log(` ${i + 1}. ${t.full_table_name}`);
console.log(` Größe: ${t.total_size} (Tabelle: ${t.table_size}, Indizes: ${t.indexes_size})`);
console.log(` Zeilen: ${parseInt(t.row_count).toLocaleString()}, Tote Zeilen: ${parseInt(t.dead_rows).toLocaleString()} (${t.dead_row_percent}%)`);
if (parseFloat(t.dead_row_percent) > 20) {
console.log(` ⚠️ Hoher Bloat-Anteil! Vacuum empfohlen.`);
}
if (t.last_vacuum || t.last_autovacuum) {
const lastVacuum = t.last_vacuum || t.last_autovacuum;
const daysSinceVacuum = Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24));
if (daysSinceVacuum > 7) {
console.log(` ⚠️ Letztes Vacuum: ${daysSinceVacuum} Tage her`);
}
}
console.log('');
});
}
}
async function checkIndexes() {
console.log('🔍 4. Indizes-Analyse\n');
// Fehlende Indizes (basierend auf pg_stat_user_tables)
const [missingIndexes] = await sequelize.query(`
SELECT
schemaname || '.' || relname as table_name,
seq_scan,
seq_tup_read,
idx_scan,
seq_tup_read / NULLIF(seq_scan, 0) as avg_seq_read
FROM pg_stat_user_tables
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
AND seq_scan > 1000
AND seq_tup_read / NULLIF(seq_scan, 0) > 1000
ORDER BY seq_tup_read DESC
LIMIT 10;
`);
if (missingIndexes.length > 0) {
console.log(' ⚠️ Tabellen mit vielen Sequential Scans (möglicherweise fehlende Indizes):');
missingIndexes.forEach(t => {
console.log(` ${t.table_name}: ${t.seq_scan} seq scans, ${parseInt(t.seq_tup_read).toLocaleString()} Zeilen gelesen`);
});
console.log('');
}
// Ungenutzte Indizes
const [unusedIndexes] = await sequelize.query(`
SELECT
schemaname || '.' || indexrelname as index_name,
schemaname || '.' || relname as table_name,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
idx_scan as scans
FROM pg_stat_user_indexes
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
AND idx_scan = 0
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
`);
if (unusedIndexes.length > 0) {
console.log(' ⚠️ Ungenutzte Indizes (> 1MB, nie verwendet):');
unusedIndexes.forEach(idx => {
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (0 Scans)`);
});
console.log('');
}
// Index Bloat
const [indexBloat] = await sequelize.query(`
SELECT
schemaname || '.' || indexrelname as index_name,
schemaname || '.' || relname as table_name,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
idx_scan as scans,
idx_tup_read as tuples_read,
idx_tup_fetch as tuples_fetched
FROM pg_stat_user_indexes
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
AND pg_relation_size(indexrelid) > 10 * 1024 * 1024 -- Größer als 10MB
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
`);
if (indexBloat.length > 0) {
console.log(' Top 10 größte Indizes:');
indexBloat.forEach(idx => {
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (${idx.scans} Scans)`);
});
console.log('');
}
}
async function checkVacuumStatus() {
console.log('🧹 5. Vacuum/Analyze Status\n');
const [vacuumStats] = await sequelize.query(`
SELECT
schemaname || '.' || relname as table_name,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze,
n_dead_tup,
n_live_tup,
CASE
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
ELSE 0
END as dead_percent
FROM pg_stat_user_tables
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
AND (
(last_vacuum IS NULL AND last_autovacuum IS NULL)
OR (last_vacuum < now() - interval '7 days' AND last_autovacuum < now() - interval '7 days')
OR n_dead_tup > 10000
)
ORDER BY n_dead_tup DESC
LIMIT 10;
`);
if (vacuumStats.length > 0) {
console.log(' ⚠️ Tabellen, die Vacuum benötigen könnten:');
vacuumStats.forEach(t => {
const lastVacuum = t.last_vacuum || t.last_autovacuum || 'Nie';
const daysSince = lastVacuum !== 'Nie'
? Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24))
: '∞';
console.log(` ${t.table_name}:`);
console.log(` Tote Zeilen: ${parseInt(t.n_dead_tup).toLocaleString()} (${t.dead_percent}%)`);
console.log(` Letztes Vacuum: ${lastVacuum} (${daysSince} Tage)`);
});
console.log('');
} else {
console.log(' ✅ Alle Tabellen sind aktuell gevacuumt\n');
}
}
async function checkLocks() {
console.log('🔒 6. Locking/Blocking\n');
const [locks] = await sequelize.query(`
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement,
blocked_activity.application_name AS blocked_app,
blocking_activity.application_name AS blocking_app
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
`);
if (locks.length > 0) {
console.log(' ⚠️ Blockierte Queries gefunden:');
locks.forEach(lock => {
console.log(` Blockiert: PID ${lock.blocked_pid} (${lock.blocked_user})`);
console.log(` Blockiert von: PID ${lock.blocking_pid} (${lock.blocking_user})`);
console.log(` Blockierte Query: ${lock.blocked_statement.substring(0, 100)}...`);
console.log(` Blockierende Query: ${lock.blocking_statement.substring(0, 100)}...\n`);
});
} else {
console.log(' ✅ Keine blockierten Queries gefunden\n');
}
// Zeige alle aktiven Locks
const [allLocks] = await sequelize.query(`
SELECT
locktype,
relation::regclass as relation,
mode,
granted,
pid
FROM pg_locks
WHERE relation IS NOT NULL
AND NOT granted
LIMIT 10;
`);
if (allLocks.length > 0) {
console.log(' ⚠️ Wartende Locks:');
allLocks.forEach(lock => {
console.log(` ${lock.locktype} auf ${lock.relation}: ${lock.mode} (PID ${lock.pid})`);
});
console.log('');
}
}
async function checkQueryStats() {
console.log('📈 7. Query-Statistiken\n');
try {
const [extension] = await sequelize.query(`
SELECT EXISTS(
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
) as exists;
`);
if (!extension[0].exists) {
console.log(' pg_stat_statements ist nicht aktiviert.\n');
return;
}
const [topQueries] = await sequelize.query(`
SELECT
left(query, 80) as query_preview,
calls,
total_exec_time,
mean_exec_time,
(100 * total_exec_time / sum(total_exec_time) OVER ()) as percent_total
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
ORDER BY calls DESC
LIMIT 5;
`);
if (topQueries.length > 0) {
console.log(' Top 5 häufigste Queries:');
topQueries.forEach((q, i) => {
console.log(` ${i + 1}. ${q.query_preview}...`);
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms`);
});
console.log('');
}
} catch (error) {
console.log(` ⚠️ Fehler: ${error.message}\n`);
}
}
async function checkConnectionPool() {
console.log('🏊 8. Connection Pool Status\n');
try {
// Hole Pool-Konfiguration aus Sequelize Config
const config = sequelize.config;
const poolConfig = config.pool || {};
console.log(` Pool-Konfiguration:`);
console.log(` Max: ${poolConfig.max || 'N/A'}`);
console.log(` Min: ${poolConfig.min || 'N/A'}`);
console.log(` Acquire Timeout: ${poolConfig.acquire || 'N/A'}ms`);
console.log(` Idle Timeout: ${poolConfig.idle || 'N/A'}ms`);
console.log(` Evict Interval: ${poolConfig.evict || 'N/A'}ms\n`);
// Versuche Pool-Status zu bekommen
const pool = sequelize.connectionManager.pool;
if (pool) {
const poolSize = pool.size || 0;
const poolUsed = pool.used || 0;
const poolPending = pool.pending || 0;
console.log(` Pool-Status:`);
console.log(` Größe: ${poolSize}`);
console.log(` Verwendet: ${poolUsed}`);
console.log(` Wartend: ${poolPending}\n`);
} else {
console.log(` Pool-Objekt nicht verfügbar\n`);
}
} catch (error) {
console.log(` ⚠️ Fehler beim Abrufen der Pool-Informationen: ${error.message}\n`);
}
}
main();

View File

@@ -0,0 +1,132 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Kurs-Tabelle
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course (
id SERIAL PRIMARY KEY,
owner_user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
language_id INTEGER NOT NULL,
difficulty_level INTEGER DEFAULT 1,
is_public BOOLEAN DEFAULT false,
share_code TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_owner_fk
FOREIGN KEY (owner_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
);
`);
// Lektionen innerhalb eines Kurses
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
id SERIAL PRIMARY KEY,
course_id INTEGER NOT NULL,
chapter_id INTEGER NOT NULL,
lesson_number INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_lesson_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_chapter_fk
FOREIGN KEY (chapter_id)
REFERENCES community.vocab_chapter(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
);
`);
// Einschreibungen in Kurse
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_enrollment_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
);
`);
// Fortschritt pro User und Lektion
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
lesson_id INTEGER NOT NULL,
completed BOOLEAN DEFAULT false,
score INTEGER DEFAULT 0,
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_course_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
);
`);
// Indizes
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
ON community.vocab_course(owner_user_id);
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
ON community.vocab_course(language_id);
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
ON community.vocab_course(is_public);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
ON community.vocab_course_lesson(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
ON community.vocab_course_lesson(chapter_id);
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
ON community.vocab_course_enrollment(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
ON community.vocab_course_enrollment(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
ON community.vocab_course_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
ON community.vocab_course_progress(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
ON community.vocab_course_progress(lesson_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS community.vocab_course_progress CASCADE;
DROP TABLE IF EXISTS community.vocab_course_enrollment CASCADE;
DROP TABLE IF EXISTS community.vocab_course_lesson CASCADE;
DROP TABLE IF EXISTS community.vocab_course CASCADE;
`);
}
};

View File

@@ -0,0 +1,101 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Grammatik-Übungstypen (z.B. "gap_fill", "multiple_choice", "sentence_building", "transformation")
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
);
`);
// Grammatik-Übungen (verknüpft mit Lektionen)
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
id SERIAL PRIMARY KEY,
lesson_id INTEGER NOT NULL,
exercise_type_id INTEGER NOT NULL,
exercise_number INTEGER NOT NULL,
title TEXT NOT NULL,
instruction TEXT,
question_data JSONB NOT NULL,
answer_data JSONB NOT NULL,
explanation TEXT,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_grammar_exercise_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_type_fk
FOREIGN KEY (exercise_type_id)
REFERENCES community.vocab_grammar_exercise_type(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
);
`);
// Fortschritt für Grammatik-Übungen
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
exercise_id INTEGER NOT NULL,
attempts INTEGER DEFAULT 0,
correct_attempts INTEGER DEFAULT 0,
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
completed BOOLEAN DEFAULT false,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_grammar_exercise_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
FOREIGN KEY (exercise_id)
REFERENCES community.vocab_grammar_exercise(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
);
`);
// Indizes
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
ON community.vocab_grammar_exercise(lesson_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
ON community.vocab_grammar_exercise(exercise_type_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
ON community.vocab_grammar_exercise_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
ON community.vocab_grammar_exercise_progress(exercise_id);
`);
// Standard-Übungstypen einfügen
await queryInterface.sequelize.query(`
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('gap_fill', 'Lückentext-Übung'),
('multiple_choice', 'Multiple-Choice-Fragen'),
('sentence_building', 'Satzbau-Übung'),
('transformation', 'Satzumformung'),
('conjugation', 'Konjugations-Übung'),
('declension', 'Deklinations-Übung')
ON CONFLICT (name) DO NOTHING;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS community.vocab_grammar_exercise_progress CASCADE;
DROP TABLE IF EXISTS community.vocab_grammar_exercise CASCADE;
DROP TABLE IF EXISTS community.vocab_grammar_exercise_type CASCADE;
`);
}
};

View File

@@ -0,0 +1,47 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// chapter_id optional machen (nicht alle Lektionen brauchen ein Kapitel)
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ALTER COLUMN chapter_id DROP NOT NULL;
`);
// Kurs-Wochen/Module hinzufügen
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS week_number INTEGER,
ADD COLUMN IF NOT EXISTS day_number INTEGER,
ADD COLUMN IF NOT EXISTS lesson_type TEXT DEFAULT 'vocab',
ADD COLUMN IF NOT EXISTS audio_url TEXT,
ADD COLUMN IF NOT EXISTS cultural_notes TEXT;
`);
// Indizes für Wochen/Tage
await queryInterface.sequelize.query(`
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
ON community.vocab_course_lesson(course_id, week_number);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
ON community.vocab_course_lesson(lesson_type);
`);
// Kommentar hinzufügen für lesson_type
await queryInterface.sequelize.query(`
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
'Type: vocab, grammar, conversation, culture, review';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS week_number,
DROP COLUMN IF EXISTS day_number,
DROP COLUMN IF EXISTS lesson_type,
DROP COLUMN IF EXISTS audio_url,
DROP COLUMN IF EXISTS cultural_notes;
`);
}
};

View File

@@ -0,0 +1,33 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
// Lernziele für Lektionen
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS target_minutes INTEGER,
ADD COLUMN IF NOT EXISTS target_score_percent INTEGER DEFAULT 80,
ADD COLUMN IF NOT EXISTS requires_review BOOLEAN DEFAULT false;
`);
// Kommentare hinzufügen
await queryInterface.sequelize.query(`
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
'Zielzeit in Minuten für diese Lektion';
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS target_minutes,
DROP COLUMN IF EXISTS target_score_percent,
DROP COLUMN IF EXISTS requires_review;
`);
}
};

View File

@@ -5,6 +5,7 @@ import ChatUser from './chat/user.js';
import Room from './chat/room.js'; import Room from './chat/room.js';
import User from './community/user.js'; import User from './community/user.js';
import UserParam from './community/user_param.js'; import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js';
import UserParamType from './type/user_param.js'; import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js'; import UserRightType from './type/user_right.js';
import UserRight from './community/user_right.js'; import UserRight from './community/user_right.js';
@@ -93,6 +94,10 @@ import PoliticalOfficeRequirement from './falukant/predefine/political_office_pr
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js'; import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
import PoliticalOfficeHistory from './falukant/log/political_office_history.js'; import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js'; import ElectionHistory from './falukant/log/election_history.js';
import ChurchOfficeType from './falukant/type/church_office_type.js';
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
import ChurchOffice from './falukant/data/church_office.js';
import ChurchApplication from './falukant/data/church_application.js';
import Underground from './falukant/data/underground.js'; import Underground from './falukant/data/underground.js';
import UndergroundType from './falukant/type/underground.js'; import UndergroundType from './falukant/type/underground.js';
import VehicleType from './falukant/type/vehicle.js'; import VehicleType from './falukant/type/vehicle.js';
@@ -102,8 +107,17 @@ import RegionDistance from './falukant/data/region_distance.js';
import WeatherType from './falukant/type/weather.js'; import WeatherType from './falukant/type/weather.js';
import Weather from './falukant/data/weather.js'; import Weather from './falukant/data/weather.js';
import ProductWeatherEffect from './falukant/type/product_weather_effect.js'; import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
import ProductPriceHistory from './falukant/log/product_price_history.js';
import Blog from './community/blog.js'; import Blog from './community/blog.js';
import BlogPost from './community/blog_post.js'; import BlogPost from './community/blog_post.js';
import VocabCourse from './community/vocab_course.js';
import VocabCourseLesson from './community/vocab_course_lesson.js';
import VocabCourseEnrollment from './community/vocab_course_enrollment.js';
import VocabCourseProgress from './community/vocab_course_progress.js';
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
import CalendarEvent from './community/calendar_event.js';
import Campaign from './match3/campaign.js'; import Campaign from './match3/campaign.js';
import Match3Level from './match3/level.js'; import Match3Level from './match3/level.js';
import Objective from './match3/objective.js'; import Objective from './match3/objective.js';
@@ -155,6 +169,9 @@ export default function setupAssociations() {
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' }); User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' }); UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasOne(UserDashboard, { foreignKey: 'userId', as: 'dashboard' });
UserDashboard.belongsTo(User, { foreignKey: 'userId', as: 'user' });
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' }); UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' }); UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
@@ -405,6 +422,13 @@ export default function setupAssociations() {
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' }); DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' }); FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' });
// Produkt-Preishistorie (Zeitreihe für Preiskurven)
ProductPriceHistory.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
ProductType.hasMany(ProductPriceHistory, { foreignKey: 'productId', as: 'priceHistory' });
ProductPriceHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(ProductPriceHistory, { foreignKey: 'regionId', as: 'productPriceHistory' });
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' }); Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' }); FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
@@ -558,14 +582,14 @@ export default function setupAssociations() {
Party.belongsToMany(TitleOfNobility, { Party.belongsToMany(TitleOfNobility, {
through: PartyInvitedNobility, through: PartyInvitedNobility,
foreignKey: 'party_id', foreignKey: 'partyId',
otherKey: 'title_of_nobility_id', otherKey: 'titleOfNobilityId',
as: 'invitedNobilities', as: 'invitedNobilities',
}); });
TitleOfNobility.belongsToMany(Party, { TitleOfNobility.belongsToMany(Party, {
through: PartyInvitedNobility, through: PartyInvitedNobility,
foreignKey: 'title_of_nobility_id', foreignKey: 'titleOfNobilityId',
otherKey: 'party_id', otherKey: 'partyId',
as: 'partiesInvitedTo', as: 'partiesInvitedTo',
}); });
@@ -859,6 +883,96 @@ export default function setupAssociations() {
} }
); );
// — Church Offices —
// Requirements for church office
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
ChurchOfficeType.hasMany(ChurchOfficeRequirement, {
foreignKey: 'officeTypeId',
as: 'requirements'
});
// Prerequisite office type
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
foreignKey: 'prerequisiteOfficeTypeId',
as: 'prerequisiteOfficeType'
});
// Actual church office holdings
ChurchOffice.belongsTo(ChurchOfficeType, {
foreignKey: 'officeTypeId',
as: 'type'
});
ChurchOfficeType.hasMany(ChurchOffice, {
foreignKey: 'officeTypeId',
as: 'offices'
});
ChurchOffice.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'holder'
});
FalukantCharacter.hasOne(ChurchOffice, {
foreignKey: 'characterId',
as: 'heldChurchOffice'
});
// Supervisor relationship
ChurchOffice.belongsTo(FalukantCharacter, {
foreignKey: 'supervisorId',
as: 'supervisor'
});
// Region relationship
ChurchOffice.belongsTo(RegionData, {
foreignKey: 'regionId',
as: 'region'
});
RegionData.hasMany(ChurchOffice, {
foreignKey: 'regionId',
as: 'churchOffices'
});
// Applications for church office
ChurchApplication.belongsTo(ChurchOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
ChurchOfficeType.hasMany(ChurchApplication, {
foreignKey: 'officeTypeId',
as: 'applications'
});
ChurchApplication.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'applicant'
});
FalukantCharacter.hasMany(ChurchApplication, {
foreignKey: 'characterId',
as: 'churchApplications'
});
ChurchApplication.belongsTo(FalukantCharacter, {
foreignKey: 'supervisorId',
as: 'supervisor'
});
FalukantCharacter.hasMany(ChurchApplication, {
foreignKey: 'supervisorId',
as: 'supervisedApplications'
});
ChurchApplication.belongsTo(RegionData, {
foreignKey: 'regionId',
as: 'region'
});
RegionData.hasMany(ChurchApplication, {
foreignKey: 'regionId',
as: 'churchApplications'
});
Underground.belongsTo(UndergroundType, { Underground.belongsTo(UndergroundType, {
foreignKey: 'undergroundTypeId', foreignKey: 'undergroundTypeId',
as: 'undergroundType' as: 'undergroundType'
@@ -941,5 +1055,41 @@ export default function setupAssociations() {
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' }); TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' }); TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' });
// Vocab Course associations
VocabCourse.belongsTo(User, { foreignKey: 'ownerUserId', as: 'owner' });
User.hasMany(VocabCourse, { foreignKey: 'ownerUserId', as: 'ownedCourses' });
VocabCourseLesson.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseLesson, { foreignKey: 'courseId', as: 'lessons' });
VocabCourseEnrollment.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabCourseEnrollment, { foreignKey: 'userId', as: 'courseEnrollments' });
VocabCourseEnrollment.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseEnrollment, { foreignKey: 'courseId', as: 'enrollments' });
VocabCourseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabCourseProgress, { foreignKey: 'userId', as: 'courseProgress' });
VocabCourseProgress.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseProgress, { foreignKey: 'courseId', as: 'progress' });
VocabCourseProgress.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabCourseProgress, { foreignKey: 'lessonId', as: 'progress' });
// Grammar Exercise associations
VocabGrammarExercise.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabGrammarExercise, { foreignKey: 'lessonId', as: 'grammarExercises' });
VocabGrammarExercise.belongsTo(VocabGrammarExerciseType, { foreignKey: 'exerciseTypeId', as: 'exerciseType' });
VocabGrammarExerciseType.hasMany(VocabGrammarExercise, { foreignKey: 'exerciseTypeId', as: 'exercises' });
VocabGrammarExercise.belongsTo(User, { foreignKey: 'createdByUserId', as: 'creator' });
User.hasMany(VocabGrammarExercise, { foreignKey: 'createdByUserId', as: 'createdGrammarExercises' });
VocabGrammarExerciseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
// Calendar associations
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(CalendarEvent, { foreignKey: 'userId', as: 'calendarEvents' });
} }

View File

@@ -0,0 +1,86 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class CalendarEvent extends Model { }
CalendarEvent.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'user',
key: 'id'
}
},
title: {
type: DataTypes.STRING(255),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
categoryId: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'personal',
comment: 'Category key: personal, work, family, health, birthday, holiday, reminder, other'
},
startDate: {
type: DataTypes.DATEONLY,
allowNull: false
},
endDate: {
type: DataTypes.DATEONLY,
allowNull: true,
comment: 'End date for multi-day events, null means same as startDate'
},
startTime: {
type: DataTypes.TIME,
allowNull: true,
comment: 'Start time, null for all-day events'
},
endTime: {
type: DataTypes.TIME,
allowNull: true,
comment: 'End time, null for all-day events'
},
allDay: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updatedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
sequelize,
modelName: 'CalendarEvent',
tableName: 'calendar_event',
schema: 'community',
timestamps: true,
underscored: true,
indexes: [
{
fields: ['user_id']
},
{
fields: ['user_id', 'start_date']
},
{
fields: ['user_id', 'start_date', 'end_date']
}
]
});
export default CalendarEvent;

View File

@@ -0,0 +1,24 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
import User from './user.js';
const UserDashboard = sequelize.define('user_dashboard', {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
references: { model: User, key: 'id' }
},
config: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: { widgets: [] }
}
}, {
tableName: 'user_dashboard',
schema: 'community',
underscored: true,
timestamps: false
});
export default UserDashboard;

View File

@@ -0,0 +1,75 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourse extends Model {}
VocabCourse.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
ownerUserId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'owner_user_id'
},
title: {
type: DataTypes.TEXT,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
languageId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'language_id'
},
nativeLanguageId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'native_language_id',
comment: 'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".'
},
difficultyLevel: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
field: 'difficulty_level'
},
isPublic: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'is_public'
},
shareCode: {
type: DataTypes.TEXT,
allowNull: true,
unique: true,
field: 'share_code'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'updated_at'
}
}, {
sequelize,
modelName: 'VocabCourse',
tableName: 'vocab_course',
schema: 'community',
timestamps: true,
underscored: true
});
export default VocabCourse;

View File

@@ -0,0 +1,37 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourseEnrollment extends Model {}
VocabCourseEnrollment.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'user_id'
},
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'course_id'
},
enrolledAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'enrolled_at'
}
}, {
sequelize,
modelName: 'VocabCourseEnrollment',
tableName: 'vocab_course_enrollment',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabCourseEnrollment;

View File

@@ -0,0 +1,93 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourseLesson extends Model {}
VocabCourseLesson.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'course_id'
},
chapterId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'chapter_id'
},
lessonNumber: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'lesson_number'
},
title: {
type: DataTypes.TEXT,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
weekNumber: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'week_number'
},
dayNumber: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'day_number'
},
lessonType: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: 'vocab',
field: 'lesson_type'
},
audioUrl: {
type: DataTypes.TEXT,
allowNull: true,
field: 'audio_url'
},
culturalNotes: {
type: DataTypes.TEXT,
allowNull: true,
field: 'cultural_notes'
},
targetMinutes: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'target_minutes'
},
targetScorePercent: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 80,
field: 'target_score_percent'
},
requiresReview: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'requires_review'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
}
}, {
sequelize,
modelName: 'VocabCourseLesson',
tableName: 'vocab_course_lesson',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabCourseLesson;

View File

@@ -0,0 +1,56 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourseProgress extends Model {}
VocabCourseProgress.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'user_id'
},
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'course_id'
},
lessonId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'lesson_id'
},
completed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
score: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
lastAccessedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_accessed_at'
},
completedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'completed_at'
}
}, {
sequelize,
modelName: 'VocabCourseProgress',
tableName: 'vocab_course_progress',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabCourseProgress;

View File

@@ -0,0 +1,69 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabGrammarExercise extends Model {}
VocabGrammarExercise.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
lessonId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'lesson_id'
},
exerciseTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'exercise_type_id'
},
exerciseNumber: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'exercise_number'
},
title: {
type: DataTypes.TEXT,
allowNull: false
},
instruction: {
type: DataTypes.TEXT,
allowNull: true
},
questionData: {
type: DataTypes.JSONB,
allowNull: false,
field: 'question_data'
},
answerData: {
type: DataTypes.JSONB,
allowNull: false,
field: 'answer_data'
},
explanation: {
type: DataTypes.TEXT,
allowNull: true
},
createdByUserId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'created_by_user_id'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
}
}, {
sequelize,
modelName: 'VocabGrammarExercise',
tableName: 'vocab_grammar_exercise',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabGrammarExercise;

View File

@@ -0,0 +1,57 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabGrammarExerciseProgress extends Model {}
VocabGrammarExerciseProgress.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'user_id'
},
exerciseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'exercise_id'
},
attempts: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
correctAttempts: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'correct_attempts'
},
lastAttemptAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_attempt_at'
},
completed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
completedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'completed_at'
}
}, {
sequelize,
modelName: 'VocabGrammarExerciseProgress',
tableName: 'vocab_grammar_exercise_progress',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabGrammarExerciseProgress;

View File

@@ -0,0 +1,36 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabGrammarExerciseType extends Model {}
VocabGrammarExerciseType.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.TEXT,
allowNull: false,
unique: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
}
}, {
sequelize,
modelName: 'VocabGrammarExerciseType',
tableName: 'vocab_grammar_exercise_type',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabGrammarExerciseType;

View File

@@ -0,0 +1,47 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchApplication extends Model {}
ChurchApplication.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false
},
characterId: {
type: DataTypes.INTEGER,
allowNull: false
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false
},
supervisorId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'ID des Vorgesetzten, der über die Bewerbung entscheidet (null für Einstiegspositionen ohne Supervisor)'
},
status: {
type: DataTypes.ENUM('pending', 'approved', 'rejected'),
allowNull: false,
defaultValue: 'pending'
},
decisionDate: {
type: DataTypes.DATE,
allowNull: true
}
}, {
sequelize,
modelName: 'ChurchApplication',
tableName: 'church_application',
schema: 'falukant_data',
timestamps: true,
underscored: true
});
export default ChurchApplication;

View File

@@ -0,0 +1,38 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchOffice extends Model {}
ChurchOffice.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false
},
characterId: {
type: DataTypes.INTEGER,
allowNull: false
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false
},
supervisorId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'ID des Vorgesetzten (höhere Position in der Hierarchie)'
}
}, {
sequelize,
modelName: 'ChurchOffice',
tableName: 'church_office',
schema: 'falukant_data',
timestamps: true,
underscored: true
});
export default ChurchOffice;

View File

@@ -22,7 +22,12 @@ Production.init({
startTimestamp: { startTimestamp: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: false, allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')} defaultValue: sequelize.literal('CURRENT_TIMESTAMP')},
sleep: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Produktion ist zurückgestellt'}
}, { }, {
sequelize, sequelize,
modelName: 'Production', modelName: 'Production',

View File

@@ -10,11 +10,20 @@ RegionData.init({
allowNull: false}, allowNull: false},
regionTypeId: { regionTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false,
references: {
model: RegionType,
key: 'id',
schema: 'falukant_type'
}
}, },
parentId: { parentId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: true,
references: {
model: 'region',
key: 'id',
schema: 'falukant_data'}
}, },
map: { map: {
type: DataTypes.JSONB, type: DataTypes.JSONB,

View File

@@ -6,7 +6,8 @@ class FalukantStock extends Model { }
FalukantStock.init({ FalukantStock.init({
branchId: { branchId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false,
defaultValue: 0
}, },
stockTypeId: { stockTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,

View File

@@ -0,0 +1,44 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
/**
* Preishistorie pro Produkt und Region (Zeitreihe für Preis-Graphen).
* Aktuell wird diese Tabelle noch nicht befüllt; sie dient nur als Grundlage.
*/
class ProductPriceHistory extends Model { }
ProductPriceHistory.init({
productId: {
type: DataTypes.INTEGER,
allowNull: false
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false
},
price: {
type: DataTypes.DECIMAL(12, 2),
allowNull: false
},
recordedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
}
}, {
sequelize,
modelName: 'ProductPriceHistory',
tableName: 'product_price_history',
schema: 'falukant_log',
timestamps: false,
underscored: true,
indexes: [
{
name: 'product_price_history_product_region_recorded_idx',
fields: ['product_id', 'region_id', 'recorded_at']
}
]
});
export default ProductPriceHistory;

View File

@@ -0,0 +1,49 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
/**
* Log aller Änderungen an relationship und marriage_proposals.
* Einträge werden ausschließlich durch DB-Trigger geschrieben und nicht gelöscht.
* Hilft zu analysieren, warum z.B. Werbungen um einen Partner verschwinden.
*/
class RelationshipChangeLog extends Model {}
RelationshipChangeLog.init(
{
changedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
tableName: {
type: DataTypes.STRING(64),
allowNull: false
},
operation: {
type: DataTypes.STRING(16),
allowNull: false
},
recordId: {
type: DataTypes.INTEGER,
allowNull: true
},
payloadOld: {
type: DataTypes.JSONB,
allowNull: true
},
payloadNew: {
type: DataTypes.JSONB,
allowNull: true
}
},
{
sequelize,
modelName: 'RelationshipChangeLog',
tableName: 'relationship_change_log',
schema: 'falukant_log',
timestamps: false,
underscored: true
}
);
export default RelationshipChangeLog;

View File

@@ -0,0 +1,35 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchOfficeRequirement extends Model {}
ChurchOfficeRequirement.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false
},
prerequisiteOfficeTypeId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Erforderliche niedrigere Position in der Hierarchie'
},
minTitleLevel: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Mindest-Titel-Level (optional)'
}
}, {
sequelize,
modelName: 'ChurchOfficeRequirement',
tableName: 'church_office_requirement',
schema: 'falukant_predefine',
timestamps: false,
underscored: true
});
export default ChurchOfficeRequirement;

View File

@@ -10,12 +10,14 @@ PromotionalGiftCharacterTrait.init(
giftId: { giftId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'gift_id', field: 'gift_id',
allowNull: false allowNull: false,
primaryKey: true
}, },
traitId: { traitId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'trait_id', field: 'trait_id',
allowNull: false allowNull: false,
primaryKey: true
}, },
suitability: { suitability: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,

View File

@@ -10,12 +10,14 @@ PromotionalGiftMood.init(
giftId: { giftId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'gift_id', field: 'gift_id',
allowNull: false allowNull: false,
primaryKey: true
}, },
moodId: { moodId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
field: 'mood_id', field: 'mood_id',
allowNull: false allowNull: false,
primaryKey: true
}, },
suitability: { suitability: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,

View File

@@ -0,0 +1,38 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchOfficeType extends Model {}
ChurchOfficeType.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
seatsPerRegion: {
type: DataTypes.INTEGER,
allowNull: false
},
regionType: {
type: DataTypes.STRING,
allowNull: false
},
hierarchyLevel: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Höhere Zahl = höhere Position in der Hierarchie'
}
}, {
sequelize,
modelName: 'ChurchOfficeType',
tableName: 'church_office_type',
schema: 'falukant_type',
timestamps: false,
underscored: true
});
export default ChurchOfficeType;

View File

@@ -15,7 +15,8 @@ ProductType.init({
allowNull: false}, allowNull: false},
sellCost: { sellCost: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false} allowNull: false
}
}, { }, {
sequelize, sequelize,
modelName: 'ProductType', modelName: 'ProductType',

View File

@@ -4,8 +4,10 @@ import SettingsType from './type/settings.js';
import UserParamValue from './type/user_param_value.js'; import UserParamValue from './type/user_param_value.js';
import UserParamType from './type/user_param.js'; import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js'; import UserRightType from './type/user_right.js';
import WidgetType from './type/widget_type.js';
import User from './community/user.js'; import User from './community/user.js';
import UserParam from './community/user_param.js'; import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js';
import Login from './logs/login.js'; import Login from './logs/login.js';
import UserRight from './community/user_right.js'; import UserRight from './community/user_right.js';
import InterestType from './type/interest.js'; import InterestType from './type/interest.js';
@@ -87,6 +89,7 @@ import Learning from './falukant/data/learning.js';
import Credit from './falukant/data/credit.js'; import Credit from './falukant/data/credit.js';
import DebtorsPrism from './falukant/data/debtors_prism.js'; import DebtorsPrism from './falukant/data/debtors_prism.js';
import HealthActivity from './falukant/log/health_activity.js'; import HealthActivity from './falukant/log/health_activity.js';
import ProductPriceHistory from './falukant/log/product_price_history.js';
// — Match3 Minigame — // — Match3 Minigame —
import Match3Campaign from './match3/campaign.js'; import Match3Campaign from './match3/campaign.js';
@@ -113,6 +116,13 @@ import Vote from './falukant/data/vote.js';
import ElectionResult from './falukant/data/election_result.js'; import ElectionResult from './falukant/data/election_result.js';
import PoliticalOfficeHistory from './falukant/log/political_office_history.js'; import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js'; import ElectionHistory from './falukant/log/election_history.js';
import RelationshipChangeLog from './falukant/log/relationship_change_log.js';
// — Kirchliche Ämter (Church) —
import ChurchOfficeType from './falukant/type/church_office_type.js';
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
import ChurchOffice from './falukant/data/church_office.js';
import ChurchApplication from './falukant/data/church_application.js';
import UndergroundType from './falukant/type/underground.js'; import UndergroundType from './falukant/type/underground.js';
import Underground from './falukant/data/underground.js'; import Underground from './falukant/data/underground.js';
import VehicleType from './falukant/type/vehicle.js'; import VehicleType from './falukant/type/vehicle.js';
@@ -129,13 +139,25 @@ import ChatRight from './chat/rights.js';
import ChatUserRight from './chat/user_rights.js'; import ChatUserRight from './chat/user_rights.js';
import RoomType from './chat/room_type.js'; import RoomType from './chat/room_type.js';
// — Vocab Courses —
import VocabCourse from './community/vocab_course.js';
import VocabCourseLesson from './community/vocab_course_lesson.js';
import VocabCourseEnrollment from './community/vocab_course_enrollment.js';
import VocabCourseProgress from './community/vocab_course_progress.js';
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
import CalendarEvent from './community/calendar_event.js';
const models = { const models = {
SettingsType, SettingsType,
UserParamValue, UserParamValue,
UserParamType, UserParamType,
UserRightType, UserRightType,
WidgetType,
User, User,
UserParam, UserParam,
UserDashboard,
Login, Login,
UserRight, UserRight,
InterestType, InterestType,
@@ -218,6 +240,7 @@ const models = {
Credit, Credit,
DebtorsPrism, DebtorsPrism,
HealthActivity, HealthActivity,
ProductPriceHistory,
RegionDistance, RegionDistance,
VehicleType, VehicleType,
Vehicle, Vehicle,
@@ -233,6 +256,11 @@ const models = {
ElectionResult, ElectionResult,
PoliticalOfficeHistory, PoliticalOfficeHistory,
ElectionHistory, ElectionHistory,
RelationshipChangeLog,
ChurchOfficeType,
ChurchOfficeRequirement,
ChurchOffice,
ChurchApplication,
UndergroundType, UndergroundType,
Underground, Underground,
WeatherType, WeatherType,
@@ -263,6 +291,18 @@ const models = {
TaxiMapTileStreet, TaxiMapTileStreet,
TaxiMapTileHouse, TaxiMapTileHouse,
TaxiHighscore, TaxiHighscore,
// Vocab Courses
VocabCourse,
VocabCourseLesson,
VocabCourseEnrollment,
VocabCourseProgress,
VocabGrammarExerciseType,
VocabGrammarExercise,
VocabGrammarExerciseProgress,
// Calendar
CalendarEvent,
}; };
export default models; export default models;

View File

@@ -350,15 +350,16 @@ export async function createTriggers() {
SELECT * FROM random_fill SELECT * FROM random_fill
), ),
-- 8) Neue Ämter anlegen und sofort zurückliefern -- 8) Neue Ämter anlegen created_at = Wahldatum (Amtsbeginn), nicht NOW()
-- damit termEnds = Amtsbeginn + termLength korrekt berechnet werden kann
created_offices AS ( created_offices AS (
INSERT INTO falukant_data.political_office INSERT INTO falukant_data.political_office
(office_type_id, character_id, created_at, updated_at, region_id) (office_type_id, character_id, created_at, updated_at, region_id)
SELECT SELECT
tp.tp_office_type_id, tp.tp_office_type_id,
fw.character_id, fw.character_id,
NOW() AS created_at, tp.tp_election_date AS created_at,
NOW() AS updated_at, tp.tp_election_date AS updated_at,
tp.tp_region_id tp.tp_region_id
FROM final_winners fw FROM final_winners fw
JOIN to_process tp JOIN to_process tp

View File

@@ -0,0 +1,39 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const WidgetType = sequelize.define('widget_type', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
label: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Anzeigename des Widgets (z. B. "Termine")'
},
endpoint: {
type: DataTypes.STRING,
allowNull: false,
comment: 'API-Pfad (z. B. "/api/termine")'
},
description: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Optionale Beschreibung'
},
orderId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'order_id',
comment: 'Sortierreihenfolge'
}
}, {
tableName: 'widget_type',
schema: 'type',
underscored: true,
timestamps: false
});
export default WidgetType;

4322
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,9 @@
"dev": "NODE_ENV=development node server.js", "dev": "NODE_ENV=development node server.js",
"start-daemon": "node daemonServer.js", "start-daemon": "node daemonServer.js",
"sync-db": "node sync-database.js", "sync-db": "node sync-database.js",
"sync-tables": "node sync-tables-only.js",
"check-connections": "node check-connections.js",
"cleanup-connections": "node cleanup-connections.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"keywords": [], "keywords": [],
@@ -37,7 +40,8 @@
"sharp": "^0.34.3", "sharp": "^0.34.3",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"ws": "^8.18.0" "ws": "^8.18.0",
"@gltf-transform/cli": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"sequelize-cli": "^6.6.2" "sequelize-cli": "^6.6.2"

View File

@@ -0,0 +1,20 @@
import { Router } from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import calendarController from '../controllers/calendarController.js';
const router = Router();
// All routes require authentication
router.get('/events', authenticate, calendarController.getEvents);
router.get('/events/:id', authenticate, calendarController.getEvent);
router.post('/events', authenticate, calendarController.createEvent);
router.put('/events/:id', authenticate, calendarController.updateEvent);
router.delete('/events/:id', authenticate, calendarController.deleteEvent);
router.get('/birthdays', authenticate, calendarController.getFriendsBirthdays);
// Widget endpoints
router.get('/widget/birthdays', authenticate, calendarController.getWidgetBirthdays);
router.get('/widget/upcoming', authenticate, calendarController.getWidgetUpcoming);
router.get('/widget/mini', authenticate, calendarController.getWidgetMiniCalendar);
export default router;

View File

@@ -0,0 +1,11 @@
import { Router } from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import dashboardController from '../controllers/dashboardController.js';
const router = Router();
router.get('/widgets', authenticate, dashboardController.getAvailableWidgets.bind(dashboardController));
router.get('/config', authenticate, dashboardController.getConfig.bind(dashboardController));
router.put('/config', authenticate, dashboardController.setConfig.bind(dashboardController));
export default router;

View File

@@ -11,6 +11,7 @@ router.get('/character/affect', falukantController.getCharacterAffect);
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName); router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
router.get('/name/randomlastname', falukantController.randomLastName); router.get('/name/randomlastname', falukantController.randomLastName);
router.get('/info', falukantController.getInfo); router.get('/info', falukantController.getInfo);
router.get('/dashboard-widget', falukantController.getDashboardWidget);
router.get('/branches/types', falukantController.getBranchTypes); router.get('/branches/types', falukantController.getBranchTypes);
router.get('/branches/:branch', falukantController.getBranch); router.get('/branches/:branch', falukantController.getBranch);
router.get('/branches', falukantController.getBranches); router.get('/branches', falukantController.getBranches);
@@ -28,6 +29,7 @@ router.get('/inventory/?:branchId', falukantController.getInventory);
router.post('/sell/all', falukantController.sellAllProducts); router.post('/sell/all', falukantController.sellAllProducts);
router.post('/sell', falukantController.sellProduct); router.post('/sell', falukantController.sellProduct);
router.post('/moneyhistory', falukantController.moneyHistory); router.post('/moneyhistory', falukantController.moneyHistory);
router.post('/moneyhistory/graph', falukantController.moneyHistoryGraph);
router.get('/storage/:branchId', falukantController.getStorage); router.get('/storage/:branchId', falukantController.getStorage);
router.post('/storage', falukantController.buyStorage); router.post('/storage', falukantController.buyStorage);
router.delete('/storage', falukantController.sellStorage); router.delete('/storage', falukantController.sellStorage);
@@ -38,14 +40,14 @@ router.get('/director/:branchId', falukantController.getDirectorForBranch);
router.get('/directors', falukantController.getAllDirectors); router.get('/directors', falukantController.getAllDirectors);
router.post('/directors', falukantController.updateDirector); router.post('/directors', falukantController.updateDirector);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal); router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.post('/family/cancel-wooing', falukantController.cancelWooing);
router.post('/family/set-heir', falukantController.setHeir); router.post('/family/set-heir', falukantController.setHeir);
router.get('/heirs/potential', falukantController.getPotentialHeirs);
router.post('/heirs/select', falukantController.selectHeir);
router.get('/family/gifts', falukantController.getGifts); router.get('/family/gifts', falukantController.getGifts);
router.get('/family/children', falukantController.getChildren); router.get('/family/children', falukantController.getChildren);
router.post('/family/gift', falukantController.sendGift); router.post('/family/gift', falukantController.sendGift);
router.get('/family', falukantController.getFamily); router.get('/family', falukantController.getFamily);
router.get('/nobility/titels', falukantController.getTitlesOfNobility); router.get('/nobility/titels', falukantController.getTitlesOfNobility);
router.get('/reputation/actions', falukantController.getReputationActions);
router.get('/houses/types', falukantController.getHouseTypes); router.get('/houses/types', falukantController.getHouseTypes);
router.get('/houses/buyable', falukantController.getBuyableHouses); router.get('/houses/buyable', falukantController.getBuyableHouses);
router.get('/houses', falukantController.getUserHouse); router.get('/houses', falukantController.getUserHouse);
@@ -55,10 +57,13 @@ router.post('/houses', falukantController.buyUserHouse);
router.get('/party/types', falukantController.getPartyTypes); router.get('/party/types', falukantController.getPartyTypes);
router.post('/party', falukantController.createParty); router.post('/party', falukantController.createParty);
router.get('/party', falukantController.getParties); router.get('/party', falukantController.getParties);
router.get('/reputation/actions', falukantController.getReputationActions);
router.post('/reputation/actions', falukantController.executeReputationAction);
router.get('/family/notbaptised', falukantController.getNotBaptisedChildren); router.get('/family/notbaptised', falukantController.getNotBaptisedChildren);
router.post('/church/baptise', falukantController.baptise); router.post('/church/baptise', falukantController.baptise);
router.get('/church/overview', falukantController.getChurchOverview);
router.get('/church/positions/available', falukantController.getAvailableChurchPositions);
router.get('/church/applications/supervised', falukantController.getSupervisedApplications);
router.post('/church/positions/apply', falukantController.applyForChurchPosition);
router.post('/church/applications/decide', falukantController.decideOnChurchApplication);
router.get('/education', falukantController.getEducation); router.get('/education', falukantController.getEducation);
router.post('/education', falukantController.sendToSchool); router.post('/education', falukantController.sendToSchool);
router.get('/bank/overview', falukantController.getBankOverview); router.get('/bank/overview', falukantController.getBankOverview);
@@ -76,8 +81,9 @@ router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections); router.post('/politics/open', falukantController.applyForElections);
router.get('/cities', falukantController.getRegions); router.get('/cities', falukantController.getRegions);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion); router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-region-batch', falukantController.getProductPricesInRegionBatch); router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities); router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.post('/products/prices-in-cities-batch', falukantController.getProductPricesInCitiesBatch);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes); router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes); router.get('/vehicles/types', falukantController.getVehicleTypes);
router.post('/vehicles', falukantController.buyVehicles); router.post('/vehicles', falukantController.buyVehicles);

View File

@@ -0,0 +1,28 @@
import express from 'express';
import path from 'path';
import { getOptimizedModelPath } from '../services/modelsProxyService.js';
const router = express.Router();
/**
* GET /api/models/3d/falukant/characters/:filename
* Liefert die Draco-optimierte GLB-Datei (aus Cache oder nach Optimierung).
*/
router.get('/3d/falukant/characters/:filename', async (req, res) => {
const { filename } = req.params;
try {
const cachePath = await getOptimizedModelPath(filename);
res.setHeader('Content-Type', 'model/gltf-binary');
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
res.sendFile(cachePath);
} catch (e) {
if (e.message?.includes('Invalid model filename') || e.message?.includes('not found')) {
return res.status(404).send(e.message);
}
console.error('[models-proxy]', e.message);
res.status(500).send('Model optimization failed');
}
});
export default router;

View File

@@ -0,0 +1,9 @@
import { Router } from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import newsController from '../controllers/newsController.js';
const router = Router();
router.get('/', authenticate, newsController.getNews.bind(newsController));
export default router;

View File

@@ -8,6 +8,7 @@ const vocabController = new VocabController();
router.use(authenticate); router.use(authenticate);
router.get('/languages', vocabController.listLanguages); router.get('/languages', vocabController.listLanguages);
router.get('/languages/all', vocabController.listAllLanguages);
router.post('/languages', vocabController.createLanguage); router.post('/languages', vocabController.createLanguage);
router.post('/subscribe', vocabController.subscribe); router.post('/subscribe', vocabController.subscribe);
router.get('/languages/:languageId', vocabController.getLanguage); router.get('/languages/:languageId', vocabController.getLanguage);
@@ -22,6 +23,39 @@ router.get('/chapters/:chapterId', vocabController.getChapter);
router.get('/chapters/:chapterId/vocabs', vocabController.listChapterVocabs); router.get('/chapters/:chapterId/vocabs', vocabController.listChapterVocabs);
router.post('/chapters/:chapterId/vocabs', vocabController.addVocabToChapter); router.post('/chapters/:chapterId/vocabs', vocabController.addVocabToChapter);
// Courses
router.post('/courses', vocabController.createCourse);
router.get('/courses', vocabController.getCourses);
router.get('/courses/my', vocabController.getMyCourses);
router.post('/courses/find-by-code', vocabController.getCourseByShareCode);
router.get('/courses/:courseId', vocabController.getCourse);
router.put('/courses/:courseId', vocabController.updateCourse);
router.delete('/courses/:courseId', vocabController.deleteCourse);
// Lessons
router.post('/courses/:courseId/lessons', vocabController.addLessonToCourse);
router.put('/lessons/:lessonId', vocabController.updateLesson);
router.delete('/lessons/:lessonId', vocabController.deleteLesson);
// Enrollment
router.post('/courses/:courseId/enroll', vocabController.enrollInCourse);
router.delete('/courses/:courseId/enroll', vocabController.unenrollFromCourse);
// Progress
router.get('/courses/:courseId/progress', vocabController.getCourseProgress);
router.get('/lessons/:lessonId', vocabController.getLesson);
router.put('/lessons/:lessonId/progress', vocabController.updateLessonProgress);
// Grammar Exercises
router.get('/grammar/exercise-types', vocabController.getExerciseTypes);
router.post('/lessons/:lessonId/grammar-exercises', vocabController.createGrammarExercise);
router.get('/lessons/:lessonId/grammar-exercises', vocabController.getGrammarExercisesForLesson);
router.get('/lessons/:lessonId/grammar-exercises/progress', vocabController.getGrammarExerciseProgress);
router.get('/grammar-exercises/:exerciseId', vocabController.getGrammarExercise);
router.post('/grammar-exercises/:exerciseId/check', vocabController.checkGrammarExerciseAnswer);
router.put('/grammar-exercises/:exerciseId', vocabController.updateGrammarExercise);
router.delete('/grammar-exercises/:exerciseId', vocabController.deleteGrammarExercise);
export default router; export default router;

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env node
/**
* Script zum Hinzufügen der Lektionen 9 und 10 (Woche 1 - Wiederholung, Woche 1 - Vokabeltest)
* zu bestehenden Bisaya-Kursen, falls diese noch fehlen.
*
* Verwendung:
* node backend/scripts/add-bisaya-week1-lessons.js
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
const LESSONS_TO_ADD = [
{
lessonNumber: 9,
weekNumber: 1,
dayNumber: 5,
lessonType: 'review',
title: 'Woche 1 - Wiederholung',
description: 'Wiederhole alle Inhalte der ersten Woche',
culturalNotes: 'Wiederholung ist der Schlüssel zum Erfolg!',
targetMinutes: 30,
targetScorePercent: 80,
requiresReview: false
},
{
lessonNumber: 10,
weekNumber: 1,
dayNumber: 5,
lessonType: 'vocab',
title: 'Woche 1 - Vokabeltest',
description: 'Teste dein Wissen aus Woche 1',
culturalNotes: null,
targetMinutes: 15,
targetScorePercent: 80,
requiresReview: true
}
];
async function addBisayaWeek1Lessons() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const courses = await sequelize.query(
`SELECT id, title FROM community.vocab_course WHERE language_id = :languageId`,
{
replacements: { languageId: bisayaLanguage.id },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Bisaya-Kurs(e)\n`);
let totalAdded = 0;
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
for (const lessonData of LESSONS_TO_ADD) {
const existing = await VocabCourseLesson.findOne({
where: {
courseId: course.id,
lessonNumber: lessonData.lessonNumber
}
});
if (existing) {
console.log(` ⏭️ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" - bereits vorhanden`);
continue;
}
await VocabCourseLesson.create({
courseId: course.id,
chapterId: null,
lessonNumber: lessonData.lessonNumber,
title: lessonData.title,
description: lessonData.description,
weekNumber: lessonData.weekNumber,
dayNumber: lessonData.dayNumber,
lessonType: lessonData.lessonType,
culturalNotes: lessonData.culturalNotes,
targetMinutes: lessonData.targetMinutes,
targetScorePercent: lessonData.targetScorePercent,
requiresReview: lessonData.requiresReview
});
console.log(` ✅ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" hinzugefügt`);
totalAdded++;
}
console.log('');
}
console.log(`\n🎉 Fertig! ${totalAdded} Lektion(en) hinzugefügt.`);
console.log('💡 Führe danach create-bisaya-course-content.js aus, um die Übungen zu erstellen.');
}
addBisayaWeek1Lessons()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* Script zum Hinzufügen von Grammatik-Übungen zu bestehenden Kursen
*
* Verwendung:
* node backend/scripts/add-grammar-exercises-to-existing-courses.js
*
* Fügt Beispiel-Grammatik-Übungen zu allen Grammar-Lektionen hinzu, die noch keine Übungen haben.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import User from '../models/community/user.js';
async function findOrCreateSystemUser() {
let systemUser = await User.findOne({
where: {
username: 'system'
}
});
if (!systemUser) {
systemUser = await User.findOne({
where: {
username: 'admin'
}
});
}
if (!systemUser) {
console.error('❌ System-Benutzer nicht gefunden. Bitte erstelle einen System-Benutzer.');
throw new Error('System user not found');
}
return systemUser;
}
// Erstelle Beispiel-Grammatik-Übungen für eine Grammar-Lektion
function createExampleGrammarExercises(lessonId, lessonTitle, ownerUserId) {
const exercises = [];
// Beispiel-Übung 1: Gap Fill (Lückentext)
exercises.push({
lessonId: lessonId,
exerciseTypeId: 1, // gap_fill
exerciseNumber: 1,
title: `${lessonTitle} - Übung 1`,
instruction: 'Fülle die Lücken mit den richtigen Wörtern.',
questionData: JSON.stringify({
type: 'gap_fill',
text: 'Hallo! Wie geht es {gap}? Mir geht es {gap}, danke!',
gaps: 2
}),
answerData: JSON.stringify({
type: 'gap_fill',
answers: ['dir', 'gut']
}),
explanation: 'Die richtigen Antworten sind "dir" und "gut".',
createdByUserId: ownerUserId
});
// Beispiel-Übung 2: Multiple Choice
exercises.push({
lessonId: lessonId,
exerciseTypeId: 2, // multiple_choice
exerciseNumber: 2,
title: `${lessonTitle} - Übung 2`,
instruction: 'Wähle die richtige Antwort aus.',
questionData: JSON.stringify({
type: 'multiple_choice',
question: 'Wie sagt man "Guten Tag"?',
options: ['Guten Tag', 'Gute Nacht', 'Auf Wiedersehen', 'Tschüss']
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: 0
}),
explanation: 'Die richtige Antwort ist "Guten Tag".',
createdByUserId: ownerUserId
});
return exercises;
}
async function addGrammarExercisesToExistingCourses() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
// Finde alle Grammar-Lektionen ohne Übungen
const grammarLessons = await sequelize.query(
`SELECT l.id, l.title, l.course_id, c.owner_user_id
FROM community.vocab_course_lesson l
JOIN community.vocab_course c ON c.id = l.course_id
WHERE l.lesson_type = 'grammar'
AND NOT EXISTS (
SELECT 1 FROM community.vocab_grammar_exercise e
WHERE e.lesson_id = l.id
)
ORDER BY l.course_id, l.lesson_number`,
{
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${grammarLessons.length} Grammar-Lektionen ohne Übungen\n`);
if (grammarLessons.length === 0) {
console.log('✅ Alle Grammar-Lektionen haben bereits Übungen.');
return;
}
let addedCount = 0;
for (const lesson of grammarLessons) {
const exercises = createExampleGrammarExercises(lesson.id, lesson.title, lesson.owner_user_id);
for (const exercise of exercises) {
await VocabGrammarExercise.create(exercise);
addedCount++;
}
console.log(`${exercises.length} Übungen zu "${lesson.title}" hinzugefügt`);
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${addedCount} Grammatik-Übungen zu ${grammarLessons.length} Lektionen hinzugefügt`);
}
addGrammarExercisesToExistingCourses()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,829 @@
#!/usr/bin/env node
/**
* Script zum Erstellen von sprachspezifischem Content für Bisaya-Kurse
*
* Verwendung:
* node backend/scripts/create-bisaya-course-content.js
*
* Erstellt Grammatik-Übungen für alle Lektionen in Bisaya-Kursen basierend auf dem Thema der Lektion.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import VocabCourse from '../models/community/vocab_course.js';
import User from '../models/community/user.js';
// Bisaya-spezifische Übungen basierend auf Lektionsthemen
const BISAYA_EXERCISES = {
// Lektion 1: Begrüßungen & Höflichkeit
'Begrüßungen & Höflichkeit': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Wie geht es dir?" auf Bisaya?',
instruction: 'Wähle die richtige Begrüßung aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wie geht es dir?" auf Bisaya?',
options: ['Kumusta ka?', 'Maayo', 'Salamat', 'Palihug']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Kumusta ka?" ist die Standard-Begrüßung auf Bisaya, ähnlich wie "Wie geht es dir?"'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Begrüßungen vervollständigen',
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Wörtern.',
questionData: {
type: 'gap_fill',
text: 'Kumusta ka? Maayo {gap} (ich). Salamat.',
gaps: 1
},
answerData: {
type: 'gap_fill',
answers: ['ko']
},
explanation: '"Maayo ko" bedeutet "Mir geht es gut". "ko" ist "ich" auf Bisaya.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Was bedeutet "Salamat"?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Salamat"?',
options: ['Danke', 'Bitte', 'Entschuldigung', 'Auf Wiedersehen']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Salamat" bedeutet "Danke" auf Bisaya.'
}
],
// Lektion 3: Familienwörter
'Familienwörter': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Mutter" auf Bisaya?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Mutter" auf Bisaya?',
options: ['Nanay', 'Tatay', 'Kuya', 'Ate']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Nanay" bedeutet "Mutter" auf Bisaya. "Mama" wird auch verwendet.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Vater" auf Bisaya?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Vater" auf Bisaya?',
options: ['Tatay', 'Nanay', 'Kuya', 'Ate']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Tatay" bedeutet "Vater" auf Bisaya. "Papa" wird auch verwendet.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "älterer Bruder" auf Bisaya?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "älterer Bruder" auf Bisaya?',
options: ['Kuya', 'Ate', 'Nanay', 'Tatay']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Kuya" bedeutet "älterer Bruder" auf Bisaya. Wird auch für respektvolle Anrede von älteren Männern verwendet.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "ältere Schwester" auf Bisaya?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "ältere Schwester" auf Bisaya?',
options: ['Ate', 'Kuya', 'Nanay', 'Tatay']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Ate" bedeutet "ältere Schwester" auf Bisaya. Wird auch für respektvolle Anrede von älteren Frauen verwendet.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Großmutter" auf Bisaya?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Großmutter" auf Bisaya?',
options: ['Lola', 'Lolo', 'Nanay', 'Tatay']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Lola" bedeutet "Großmutter" auf Bisaya.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Großvater" auf Bisaya?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Großvater" auf Bisaya?',
options: ['Lolo', 'Lola', 'Nanay', 'Tatay']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Lolo" bedeutet "Großvater" auf Bisaya.'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Familienwörter vervollständigen',
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Familienwörtern.',
questionData: {
type: 'gap_fill',
text: '{gap} (Mutter) | {gap} (Vater) | {gap} (älterer Bruder) | {gap} (ältere Schwester) | {gap} (Großmutter) | {gap} (Großvater)',
gaps: 6
},
answerData: {
type: 'gap_fill',
answers: ['Nanay', 'Tatay', 'Kuya', 'Ate', 'Lola', 'Lolo']
},
explanation: 'Nanay = Mutter, Tatay = Vater, Kuya = älterer Bruder, Ate = ältere Schwester, Lola = Großmutter, Lolo = Großvater.'
},
{
exerciseTypeId: 4, // transformation
title: 'Familienwörter übersetzen',
instruction: 'Übersetze das Familienwort ins Bisaya.',
questionData: {
type: 'transformation',
text: 'Mutter',
sourceLanguage: 'Deutsch',
targetLanguage: 'Bisaya'
},
answerData: {
type: 'transformation',
correct: 'Nanay',
alternatives: ['Mama', 'Nanay', 'Inahan']
},
explanation: '"Nanay" oder "Mama" bedeutet "Mutter" auf Bisaya.'
}
],
// Lektion 15: Zeitformen - Grundlagen
'Zeitformen - Grundlagen': [
{
exerciseTypeId: 4, // transformation
title: 'Vergangenheit verstehen',
instruction: 'Was bedeutet "Ni-kaon ko"?',
questionData: {
type: 'transformation',
text: 'Ni-kaon ko',
sourceLanguage: 'Bisaya',
targetLanguage: 'Deutsch'
},
answerData: {
type: 'transformation',
correct: 'Ich habe gegessen',
alternatives: ['I ate', 'I have eaten']
},
explanation: 'Das Präfix "Ni-" zeigt die Vergangenheit an. "Ni-kaon ko" bedeutet "Ich habe gegessen".'
},
{
exerciseTypeId: 4, // transformation
title: 'Zukunft verstehen',
instruction: 'Was bedeutet "Mo-kaon ko"?',
questionData: {
type: 'transformation',
text: 'Mo-kaon ko',
sourceLanguage: 'Bisaya',
targetLanguage: 'Deutsch'
},
answerData: {
type: 'transformation',
correct: 'Ich werde essen',
alternatives: ['I will eat', 'I am going to eat']
},
explanation: 'Das Präfix "Mo-" zeigt die Zukunft an. "Mo-kaon ko" bedeutet "Ich werde essen".'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Zeitformen erkennen',
instruction: 'Setze die richtigen Präfixe ein.',
questionData: {
type: 'gap_fill',
text: '{gap}-kaon ko (Vergangenheit) | {gap}-kaon ko (Zukunft)',
gaps: 2
},
answerData: {
type: 'gap_fill',
answers: ['Ni', 'Mo']
},
explanation: 'Ni- für Vergangenheit, Mo- für Zukunft.'
}
],
// Lektion 25: Höflichkeitsformen
'Höflichkeitsformen': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Bitte langsam"?',
instruction: 'Wähle die höfliche Form aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Bitte langsam" auf Bisaya?',
options: ['Hinay-hinay lang', 'Palihug', 'Salamat', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Hinay-hinay lang" bedeutet "Bitte langsam" und ist sehr höflich.'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Höfliche Sätze vervollständigen',
instruction: 'Fülle die Lücken mit höflichen Wörtern.',
questionData: {
type: 'gap_fill',
text: '{gap} lang (Bitte langsam), wala ko kasabot. {gap} ka mubalik? (Bitte wiederholen)',
gaps: 2
},
answerData: {
type: 'gap_fill',
answers: ['Hinay-hinay', 'Palihug']
},
explanation: '"Hinay-hinay lang" = "Bitte langsam", "Palihug" = "Bitte".'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Was bedeutet "Palihug"?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Palihug"?',
options: ['Bitte', 'Danke', 'Entschuldigung', 'Gern geschehen']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Palihug" bedeutet "Bitte" auf Bisaya und wird für höfliche Bitten verwendet.'
}
],
// Lektion: Überlebenssätze
'Überlebenssätze': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Ich verstehe nicht"?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
options: ['Wala ko kasabot', 'Palihug', 'Salamat', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht" - sehr wichtig für Anfänger!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Kannst du das wiederholen?"?',
instruction: 'Wähle die richtige Bitte aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Kannst du das wiederholen?" auf Bisaya?',
options: ['Palihug ka mubalik?', 'Salamat', 'Maayo', 'Kumusta ka?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?" - essentiell für das Lernen!'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Überlebenssätze vervollständigen',
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Wörtern.',
questionData: {
type: 'gap_fill',
text: '{gap} ko kasabot (Ich verstehe nicht). {gap} ka mubalik? (Bitte wiederholen) {gap} lang (Bitte langsam).',
gaps: 3
},
answerData: {
type: 'gap_fill',
answers: ['Wala', 'Palihug', 'Hinay-hinay']
},
explanation: '"Wala ko kasabot" = "Ich verstehe nicht", "Palihug ka mubalik?" = "Bitte wiederholen", "Hinay-hinay lang" = "Bitte langsam".'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Wo ist...?"?',
instruction: 'Wähle die richtige Frage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wo ist die Toilette?" auf Bisaya?',
options: ['Asa ang CR?', 'Kumusta ka?', 'Salamat', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Asa ang CR?" bedeutet "Wo ist die Toilette?" - "Asa" = "Wo", "CR" = "Comfort Room" (Toilette).'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Wie viel kostet das?"?',
instruction: 'Wähle die richtige Frage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wie viel kostet das?" auf Bisaya?',
options: ['Tagpila ni?', 'Asa ni?', 'Unsa ni?', 'Kinsa ni?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Wichtige Fragen bilden',
instruction: 'Fülle die Lücken mit den richtigen Fragewörtern.',
questionData: {
type: 'gap_fill',
text: '{gap} ang CR? (Wo ist die Toilette?) | {gap} ni? (Wie viel kostet das?) | {gap} ni? (Was ist das?)',
gaps: 3
},
answerData: {
type: 'gap_fill',
answers: ['Asa', 'Tagpila', 'Unsa']
},
explanation: '"Asa" = "Wo", "Tagpila" = "Wie viel", "Unsa" = "Was".'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Entschuldigung"?',
instruction: 'Wähle die richtige Entschuldigung aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Entschuldigung" auf Bisaya?',
options: ['Pasensya', 'Salamat', 'Palihug', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
},
{
exerciseTypeId: 4, // transformation
title: 'Überlebenssätze übersetzen',
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: {
type: 'transformation',
text: 'Ich spreche kein Bisaya',
sourceLanguage: 'Deutsch',
targetLanguage: 'Bisaya'
},
answerData: {
type: 'transformation',
correct: 'Dili ko mag-Bisaya',
alternatives: ['Wala ko mag-Bisaya', 'Dili ko makasabot Bisaya']
},
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
}
],
// Auch für "Überlebenssätze - Teil 1" und "Überlebenssätze - Teil 2"
'Überlebenssätze - Teil 1': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Ich verstehe nicht"?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
options: ['Wala ko kasabot', 'Palihug', 'Salamat', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht" - sehr wichtig für Anfänger!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Kannst du das wiederholen?"?',
instruction: 'Wähle die richtige Bitte aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Kannst du das wiederholen?" auf Bisaya?',
options: ['Palihug ka mubalik?', 'Salamat', 'Maayo', 'Kumusta ka?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?" - essentiell für das Lernen!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Wo ist...?"?',
instruction: 'Wähle die richtige Frage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wo ist die Toilette?" auf Bisaya?',
options: ['Asa ang CR?', 'Kumusta ka?', 'Salamat', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Asa ang CR?" bedeutet "Wo ist die Toilette?" - "Asa" = "Wo", "CR" = "Comfort Room" (Toilette).'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Überlebenssätze vervollständigen',
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Wörtern.',
questionData: {
type: 'gap_fill',
text: '{gap} ko kasabot (Ich verstehe nicht). {gap} ka mubalik? (Bitte wiederholen)',
gaps: 2
},
answerData: {
type: 'gap_fill',
answers: ['Wala', 'Palihug']
},
explanation: '"Wala ko kasabot" = "Ich verstehe nicht", "Palihug ka mubalik?" = "Bitte wiederholen".'
}
],
'Überlebenssätze - Teil 2': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Wie viel kostet das?"?',
instruction: 'Wähle die richtige Frage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wie viel kostet das?" auf Bisaya?',
options: ['Tagpila ni?', 'Asa ni?', 'Unsa ni?', 'Kinsa ni?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Entschuldigung"?',
instruction: 'Wähle die richtige Entschuldigung aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Entschuldigung" auf Bisaya?',
options: ['Pasensya', 'Salamat', 'Palihug', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Wichtige Fragen bilden',
instruction: 'Fülle die Lücken mit den richtigen Fragewörtern.',
questionData: {
type: 'gap_fill',
text: '{gap} ni? (Wie viel kostet das?) | {gap} ni? (Was ist das?) | {gap} lang (Bitte langsam)',
gaps: 3
},
answerData: {
type: 'gap_fill',
answers: ['Tagpila', 'Unsa', 'Hinay-hinay']
},
explanation: '"Tagpila" = "Wie viel", "Unsa" = "Was", "Hinay-hinay lang" = "Bitte langsam".'
},
{
exerciseTypeId: 4, // transformation
title: 'Überlebenssätze übersetzen',
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: {
type: 'transformation',
text: 'Ich spreche kein Bisaya',
sourceLanguage: 'Deutsch',
targetLanguage: 'Bisaya'
},
answerData: {
type: 'transformation',
correct: 'Dili ko mag-Bisaya',
alternatives: ['Wala ko mag-Bisaya', 'Dili ko makasabot Bisaya']
},
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
}
],
// Woche 1 - Wiederholung (Lektion 9)
'Woche 1 - Wiederholung': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wiederholung: Wie sagt man "Wie geht es dir?"?',
instruction: 'Wähle die richtige Begrüßung aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wie geht es dir?" auf Bisaya?',
options: ['Kumusta ka?', 'Maayo', 'Salamat', 'Palihug']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Kumusta ka?" ist die Standard-Begrüßung auf Bisaya.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wiederholung: Wie sagt man "Mutter" auf Bisaya?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Mutter" auf Bisaya?',
options: ['Nanay', 'Tatay', 'Kuya', 'Ate']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Nanay" bedeutet "Mutter" auf Bisaya.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wiederholung: Was bedeutet "Palangga taka"?',
instruction: 'Wähle die richtige Bedeutung.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Palangga taka"?',
options: ['Ich hab dich lieb', 'Danke', 'Guten Tag', 'Auf Wiedersehen']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Palangga taka" bedeutet "Ich hab dich lieb" - wärmer als "I love you" im Familienkontext.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wiederholung: Was fragt man mit "Nikaon ka?"?',
instruction: 'Wähle die richtige Bedeutung.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Nikaon ka?"?',
options: ['Hast du schon gegessen?', 'Wie geht es dir?', 'Danke', 'Bitte']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Nikaon ka?" bedeutet "Hast du schon gegessen?" - typisch fürsorglich auf den Philippinen.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wiederholung: Wie sagt man "Ich verstehe nicht"?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
options: ['Wala ko kasabot', 'Salamat', 'Maayo', 'Palihug']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht".'
}
],
// Woche 1 - Vokabeltest (Lektion 10)
'Woche 1 - Vokabeltest': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Vokabeltest: Kumusta',
instruction: 'Was bedeutet "Kumusta"?',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Kumusta"?',
options: ['Wie geht es dir?', 'Danke', 'Bitte', 'Auf Wiedersehen']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Kumusta" kommt von spanisch "¿Cómo está?" - "Wie geht es dir?"'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Vokabeltest: Lola',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Lola"?',
options: ['Großmutter', 'Großvater', 'Mutter', 'Vater']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Lola" = Großmutter, "Lolo" = Großvater.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Vokabeltest: Salamat',
instruction: 'Wähle die richtige Bedeutung.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Salamat"?',
options: ['Danke', 'Bitte', 'Entschuldigung', 'Gern geschehen']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Salamat" bedeutet "Danke".'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Vokabeltest: Lami',
instruction: 'Was bedeutet "Lami"?',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Lami"?',
options: ['Lecker', 'Viel', 'Gut', 'Schnell']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Lami" bedeutet "lecker" oder "schmackhaft" - wichtig beim Essen!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Vokabeltest: Mingaw ko nimo',
instruction: 'Wähle die richtige Bedeutung.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Mingaw ko nimo"?',
options: ['Ich vermisse dich', 'Ich freue mich', 'Ich mag dich', 'Ich liebe dich']
},
answerData: { type: 'multiple_choice', correctAnswer: 0 },
explanation: '"Mingaw ko nimo" bedeutet "Ich vermisse dich".'
}
]
};
async function findOrCreateSystemUser() {
let systemUser = await User.findOne({
where: {
username: 'system'
}
});
if (!systemUser) {
systemUser = await User.findOne({
where: {
username: 'admin'
}
});
}
if (!systemUser) {
console.error('❌ System-Benutzer nicht gefunden.');
throw new Error('System user not found');
}
return systemUser;
}
function getExercisesForLesson(lessonTitle) {
// Suche nach exaktem Titel
if (BISAYA_EXERCISES[lessonTitle]) {
return BISAYA_EXERCISES[lessonTitle];
}
// Fallback: Suche nach Teilstring
for (const [key, exercises] of Object.entries(BISAYA_EXERCISES)) {
if (lessonTitle.includes(key) || key.includes(lessonTitle)) {
return exercises;
}
}
// Keine Übungen für unbekannte Lektionen (statt Dummy-Übungen)
return [];
}
async function createBisayaCourseContent() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
// Finde alle Bisaya-Kurse
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const courses = await sequelize.query(
`SELECT id, title, owner_user_id AS "ownerUserId" FROM community.vocab_course WHERE language_id = :languageId`,
{
replacements: { languageId: bisayaLanguage.id },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
let totalExercisesAdded = 0;
let totalLessonsProcessed = 0;
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
const lessons = await VocabCourseLesson.findAll({
where: { courseId: course.id },
order: [['lessonNumber', 'ASC']]
});
console.log(` ${lessons.length} Lektionen gefunden\n`);
for (const lesson of lessons) {
const exercises = getExercisesForLesson(lesson.title);
if (exercises.length === 0) {
const existingCount = await VocabGrammarExercise.count({ where: { lessonId: lesson.id } });
if (existingCount > 0) {
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
} else {
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`);
}
continue;
}
// Bei Woche-1-Wiederholung/Vokabeltest: Alte Platzhalter entfernen und ersetzen
const replacePlaceholders = ['Woche 1 - Wiederholung', 'Woche 1 - Vokabeltest'].includes(lesson.title);
const existingCount = await VocabGrammarExercise.count({
where: { lessonId: lesson.id }
});
if (existingCount > 0 && !replacePlaceholders) {
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
continue;
}
if (replacePlaceholders && existingCount > 0) {
const deleted = await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } });
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} Platzhalter entfernt`);
}
// Erstelle Übungen
let exerciseNumber = 1;
for (const exerciseData of exercises) {
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: exerciseData.exerciseTypeId,
exerciseNumber: exerciseNumber++,
title: exerciseData.title,
instruction: exerciseData.instruction,
questionData: JSON.stringify(exerciseData.questionData),
answerData: JSON.stringify(exerciseData.answerData),
explanation: exerciseData.explanation,
createdByUserId: course.ownerUserId || systemUser.id
});
totalExercisesAdded++;
}
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} Übung(en) erstellt`);
totalLessonsProcessed++;
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalLessonsProcessed} Lektionen bearbeitet`);
console.log(` ${totalExercisesAdded} Grammatik-Übungen erstellt`);
}
createBisayaCourseContent()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,309 @@
#!/usr/bin/env node
/**
* Script zum Erstellen eines vollständigen 4-Wochen Bisaya-Kurses
*
* Verwendung:
* node backend/scripts/create-bisaya-course.js <languageId> <ownerHashedId>
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourse from '../models/community/vocab_course.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import User from '../models/community/user.js';
import crypto from 'crypto';
const LESSONS = [
// WOCHE 1: Grundlagen & Aussprache
{ week: 1, day: 1, num: 1, type: 'conversation', title: 'Begrüßungen & Höflichkeit',
desc: 'Lerne die wichtigsten Begrüßungen und Höflichkeitsformeln',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Philippiner schätzen Höflichkeit sehr. Lächeln ist wichtig!' },
{ week: 1, day: 1, num: 2, type: 'vocab', title: 'Überlebenssätze - Teil 1',
desc: 'Die 10 wichtigsten Sätze für den Alltag',
targetMin: 20, targetScore: 85, review: true,
cultural: 'Diese Sätze helfen dir sofort im Alltag weiter.' },
{ week: 1, day: 2, num: 3, type: 'vocab', title: 'Familienwörter',
desc: 'Mama, Papa, Kuya, Ate, Lola, Lolo und mehr',
targetMin: 20, targetScore: 85, review: true,
cultural: 'Kuya und Ate werden auch für Nicht-Verwandte verwendet sehr respektvoll!' },
{ week: 1, day: 2, num: 4, type: 'conversation', title: 'Familien-Gespräche',
desc: 'Einfache Gespräche mit Familienmitgliedern',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Familienkonversationen sind herzlicher als formelle Gespräche.' },
{ week: 1, day: 3, num: 5, type: 'conversation', title: 'Gefühle & Zuneigung',
desc: 'Mingaw ko nimo, Palangga taka und mehr',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Palangga taka ist wärmer als "I love you" im Familienkontext.' },
{ week: 1, day: 3, num: 6, type: 'vocab', title: 'Überlebenssätze - Teil 2',
desc: 'Weitere wichtige Alltagssätze',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 1, day: 4, num: 7, type: 'conversation', title: 'Essen & Fürsorge',
desc: 'Nikaon ka? Kaon ta! Lami!',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Essen = Liebe! "Nikaon na ka?" ist sehr fürsorglich.' },
{ week: 1, day: 4, num: 8, type: 'vocab', title: 'Essen & Trinken',
desc: 'Wichtige Wörter rund ums Essen',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 1, day: 5, num: 9, type: 'review', title: 'Woche 1 - Wiederholung',
desc: 'Wiederhole alle Inhalte der ersten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: 'Wiederholung ist der Schlüssel zum Erfolg!' },
{ week: 1, day: 5, num: 10, type: 'vocab', title: 'Woche 1 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 1',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 2: Alltag & Familie
{ week: 2, day: 1, num: 11, type: 'conversation', title: 'Alltagsgespräche - Teil 1',
desc: 'Wie war dein Tag? Was machst du?',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' },
{ week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie',
desc: 'Balay, Kwarto, Kusina, Pamilya',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2',
desc: 'Wohin gehst du? Was machst du heute?',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung',
desc: 'Asa, dinhi, didto, padulong',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen',
desc: 'Ni-kaon ko, Mo-kaon ko - Vergangenheit und Zukunft',
targetMin: 25, targetScore: 75, review: true,
cultural: 'Cebuano hat keine komplexen Zeiten wie Deutsch. Zeit wird mit Präfixen ausgedrückt.' },
{ week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum',
desc: 'Karon, ugma, gahapon, karon adlaw',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise',
desc: 'Tagpila ni? Pwede barato?',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Handeln ist in den Philippinen üblich und erwartet.' },
{ week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise',
desc: '1-100, Preise, Mengen',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung',
desc: 'Wiederhole alle Inhalte der zweiten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 2',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 3: Vertiefung
{ week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen',
desc: 'Nalipay, nasubo, nahadlok, naguol',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' },
{ week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen',
desc: 'Wörter für verschiedene Gefühle',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden',
desc: 'Sakit, maayo, tambal, doktor',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit',
desc: 'Wörter rund um den Körper und Gesundheit',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen',
desc: 'Palihug, Pwede, Tabang',
targetMin: 20, targetScore: 75, review: true,
cultural: 'Höflichkeit ist extrem wichtig in der philippinischen Kultur.' },
{ week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen',
desc: 'Wie man höflich fragt und bittet',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie',
desc: 'Gespräche mit und über Kinder',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Kinder sind sehr wichtig in philippinischen Familien.' },
{ week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel',
desc: 'Wörter für Kinder und Spielsachen',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung',
desc: 'Wiederhole alle Inhalte der dritten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 3',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 4: Freies Sprechen
{ week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1',
desc: 'Übe freies Sprechen zu verschiedenen Themen',
targetMin: 20, targetScore: 75, review: false,
cultural: 'Fehler sind okay! Philippiner schätzen das Bemühen.' },
{ week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2',
desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2',
desc: 'Weitere Übung im freien Sprechen',
targetMin: 20, targetScore: 75, review: false,
cultural: null },
{ week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3',
desc: 'Wiederhole wichtige Vokabeln aus Woche 3',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche',
desc: 'Längere Gespräche zu verschiedenen Themen',
targetMin: 25, targetScore: 75, review: false,
cultural: 'Je mehr du sprichst, desto besser wirst du!' },
{ week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung',
desc: 'Wiederhole alle wichtigen Inhalte des Kurses',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung',
desc: 'Simuliere echte Gesprächssituationen',
targetMin: 25, targetScore: 75, review: false,
cultural: null },
{ week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln',
desc: 'Finaler Vokabeltest über den gesamten Kurs',
targetMin: 20, targetScore: 80, review: true,
cultural: null },
{ week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung',
desc: 'Finale Prüfung über alle Kursinhalte',
targetMin: 30, targetScore: 80, review: false,
cultural: 'Gratulation zum Abschluss des Kurses!' },
{ week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks',
desc: 'Wichtige kulturelle Hinweise für den Alltag',
targetMin: 15, targetScore: 0, review: false,
cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' }
];
async function createBisayaCourse(languageId, ownerHashedId) {
try {
// Finde User
const user = await User.findOne({ where: { hashedId: ownerHashedId } });
if (!user) {
throw new Error(`User mit hashedId ${ownerHashedId} nicht gefunden`);
}
// Prüfe, ob Sprache existiert
const [lang] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE id = :langId`,
{ replacements: { langId: languageId }, type: sequelize.QueryTypes.SELECT }
);
if (!lang) {
throw new Error(`Sprache mit ID ${languageId} nicht gefunden`);
}
// Erstelle Kurs
const shareCode = crypto.randomBytes(8).toString('hex');
const course = await VocabCourse.create({
ownerUserId: user.id,
title: 'Bisaya für Familien - Schnellstart in 4 Wochen',
description: 'Lerne Bisaya (Cebuano) schnell und praktisch für den Familienalltag. Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.',
languageId: Number(languageId),
difficultyLevel: 1,
isPublic: true,
shareCode
});
console.log(`✅ Kurs erstellt: ${course.id} - "${course.title}"`);
console.log(` Share-Code: ${shareCode}`);
// Erstelle Lektionen
for (const lessonData of LESSONS) {
const lesson = await VocabCourseLesson.create({
courseId: course.id,
chapterId: null, // Wird später mit Vokabeln verknüpft
lessonNumber: lessonData.num,
title: lessonData.title,
description: lessonData.desc,
weekNumber: lessonData.week,
dayNumber: lessonData.day,
lessonType: lessonData.type,
culturalNotes: lessonData.cultural,
targetMinutes: lessonData.targetMin,
targetScorePercent: lessonData.targetScore,
requiresReview: lessonData.review
});
console.log(` ✅ Lektion ${lessonData.num}: ${lessonData.title} (Woche ${lessonData.week}, Tag ${lessonData.day})`);
}
console.log(`\n🎉 Kurs erfolgreich erstellt mit ${LESSONS.length} Lektionen!`);
console.log(`\n📊 Kurs-Statistik:`);
console.log(` - Gesamte Lektionen: ${LESSONS.length}`);
console.log(` - Vokabel-Lektionen: ${LESSONS.filter(l => l.type === 'vocab').length}`);
console.log(` - Konversations-Lektionen: ${LESSONS.filter(l => l.type === 'conversation').length}`);
console.log(` - Grammatik-Lektionen: ${LESSONS.filter(l => l.type === 'grammar').length}`);
console.log(` - Wiederholungs-Lektionen: ${LESSONS.filter(l => l.type === 'review').length}`);
console.log(` - Durchschnittliche Zeit pro Tag: ~${Math.round(LESSONS.reduce((sum, l) => sum + l.targetMin, 0) / (4 * 5))} Minuten`);
console.log(`\n💡 Nächste Schritte:`);
console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`);
console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`);
console.log(` 3. Teile den Kurs mit anderen (Share-Code: ${shareCode})`);
return course;
} catch (error) {
console.error('❌ Fehler beim Erstellen des Kurses:', error);
throw error;
}
}
// CLI-Aufruf
const languageId = process.argv[2];
const ownerHashedId = process.argv[3];
if (!languageId || !ownerHashedId) {
console.error('Verwendung: node create-bisaya-course.js <languageId> <ownerHashedId>');
console.error('Beispiel: node create-bisaya-course.js 1 abc123def456');
process.exit(1);
}
createBisayaCourse(languageId, ownerHashedId)
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,557 @@
#!/usr/bin/env node
/**
* Script zum Erstellen von öffentlichen Sprachkursen für verschiedene Sprachen
*
* Verwendung:
* node backend/scripts/create-language-courses.js
*
* Erstellt öffentliche Kurse für alle Kombinationen von:
* - Zielsprachen: Bisaya, Französisch, Spanisch, Latein, Italienisch, Portugiesisch, Tagalog
* - Muttersprachen: Deutsch, Englisch, Spanisch, Französisch, Italienisch, Portugiesisch
*
* Die Kurse werden automatisch einem System-Benutzer zugeordnet und sind öffentlich zugänglich.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourse from '../models/community/vocab_course.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import User from '../models/community/user.js';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
// Kursstruktur für alle Sprachen (4 Wochen, 40 Lektionen)
const LESSON_TEMPLATE = [
// WOCHE 1: Grundlagen & Aussprache
{ week: 1, day: 1, num: 1, type: 'conversation', title: 'Begrüßungen & Höflichkeit',
desc: 'Lerne die wichtigsten Begrüßungen und Höflichkeitsformeln',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Höflichkeit ist wichtig. Lächeln hilft!' },
{ week: 1, day: 1, num: 2, type: 'vocab', title: 'Überlebenssätze - Teil 1',
desc: 'Die 10 wichtigsten Sätze für den Alltag',
targetMin: 20, targetScore: 85, review: true,
cultural: 'Diese Sätze helfen dir sofort im Alltag weiter.' },
{ week: 1, day: 2, num: 3, type: 'vocab', title: 'Familienwörter',
desc: 'Mama, Papa, Geschwister, Großeltern und mehr',
targetMin: 20, targetScore: 85, review: true,
cultural: 'Familienwörter sind wichtig für echte Gespräche.' },
{ week: 1, day: 2, num: 4, type: 'conversation', title: 'Familien-Gespräche',
desc: 'Einfache Gespräche mit Familienmitgliedern',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Familienkonversationen sind herzlicher als formelle Gespräche.' },
{ week: 1, day: 3, num: 5, type: 'conversation', title: 'Gefühle & Zuneigung',
desc: 'Wie man Gefühle ausdrückt',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Gefühle auszudrücken ist wichtig für echte Verbindung.' },
{ week: 1, day: 3, num: 6, type: 'vocab', title: 'Überlebenssätze - Teil 2',
desc: 'Weitere wichtige Alltagssätze',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 1, day: 4, num: 7, type: 'conversation', title: 'Essen & Fürsorge',
desc: 'Gespräche rund ums Essen',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Essen verbindet Menschen überall auf der Welt.' },
{ week: 1, day: 4, num: 8, type: 'vocab', title: 'Essen & Trinken',
desc: 'Wichtige Wörter rund ums Essen',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 1, day: 5, num: 9, type: 'review', title: 'Woche 1 - Wiederholung',
desc: 'Wiederhole alle Inhalte der ersten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: 'Wiederholung ist der Schlüssel zum Erfolg!' },
{ week: 1, day: 5, num: 10, type: 'vocab', title: 'Woche 1 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 1',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 2: Alltag & Familie
{ week: 2, day: 1, num: 11, type: 'conversation', title: 'Alltagsgespräche - Teil 1',
desc: 'Wie war dein Tag? Was machst du?',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' },
{ week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie',
desc: 'Wörter für Haus, Zimmer, Familie',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2',
desc: 'Wohin gehst du? Was machst du heute?',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung',
desc: 'Wo, hier, dort, gehen zu',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen',
desc: 'Vergangenheit, Gegenwart, Zukunft',
targetMin: 25, targetScore: 75, review: true,
cultural: 'Zeitformen sind wichtig für präzise Kommunikation.' },
{ week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum',
desc: 'Jetzt, morgen, gestern, heute',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise',
desc: 'Wie viel kostet das? Kann es billiger sein?',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Einkaufen ist eine wichtige Alltagssituation.' },
{ week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise',
desc: 'Zahlen 1-100, Preise, Mengen',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung',
desc: 'Wiederhole alle Inhalte der zweiten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 2',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 3: Vertiefung
{ week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen',
desc: 'Wie man verschiedene Gefühle ausdrückt',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' },
{ week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen',
desc: 'Wörter für verschiedene Gefühle',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden',
desc: 'Gespräche über Gesundheit',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit',
desc: 'Wörter rund um den Körper und Gesundheit',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen',
desc: 'Wie man höflich spricht',
targetMin: 20, targetScore: 75, review: true,
cultural: 'Höflichkeit ist extrem wichtig in jeder Kultur.' },
{ week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen',
desc: 'Wie man höflich fragt und bittet',
targetMin: 15, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie',
desc: 'Gespräche mit und über Kinder',
targetMin: 15, targetScore: 80, review: false,
cultural: 'Kinder sind sehr wichtig in Familien.' },
{ week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel',
desc: 'Wörter für Kinder und Spielsachen',
targetMin: 20, targetScore: 85, review: true,
cultural: null },
{ week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung',
desc: 'Wiederhole alle Inhalte der dritten Woche',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest',
desc: 'Teste dein Wissen aus Woche 3',
targetMin: 15, targetScore: 80, review: true,
cultural: null },
// WOCHE 4: Freies Sprechen
{ week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1',
desc: 'Übe freies Sprechen zu verschiedenen Themen',
targetMin: 20, targetScore: 75, review: false,
cultural: 'Fehler sind okay! Muttersprachler schätzen das Bemühen.' },
{ week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2',
desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2',
desc: 'Weitere Übung im freien Sprechen',
targetMin: 20, targetScore: 75, review: false,
cultural: null },
{ week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3',
desc: 'Wiederhole wichtige Vokabeln aus Woche 3',
targetMin: 25, targetScore: 85, review: true,
cultural: null },
{ week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche',
desc: 'Längere Gespräche zu verschiedenen Themen',
targetMin: 25, targetScore: 75, review: false,
cultural: 'Je mehr du sprichst, desto besser wirst du!' },
{ week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung',
desc: 'Wiederhole alle wichtigen Inhalte des Kurses',
targetMin: 30, targetScore: 80, review: false,
cultural: null },
{ week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung',
desc: 'Simuliere echte Gesprächssituationen',
targetMin: 25, targetScore: 75, review: false,
cultural: null },
{ week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln',
desc: 'Finaler Vokabeltest über den gesamten Kurs',
targetMin: 20, targetScore: 80, review: true,
cultural: null },
{ week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung',
desc: 'Finale Prüfung über alle Kursinhalte',
targetMin: 30, targetScore: 80, review: false,
cultural: 'Gratulation zum Abschluss des Kurses!' },
{ week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks',
desc: 'Wichtige kulturelle Hinweise für den Alltag',
targetMin: 15, targetScore: 0, review: false,
cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' }
];
// Zielsprachen (die zu lernenden Sprachen)
const TARGET_LANGUAGES = [
'Bisaya',
'Französisch',
'Spanisch',
'Latein',
'Italienisch',
'Portugiesisch',
'Tagalog'
];
// Muttersprachen (für die Kurse erstellt werden)
const NATIVE_LANGUAGES = [
'Deutsch',
'Englisch',
'Spanisch',
'Französisch',
'Italienisch',
'Portugiesisch'
];
// Generiere Kurskonfigurationen für alle Kombinationen
function generateCourseConfigs() {
const configs = [];
for (const targetLang of TARGET_LANGUAGES) {
for (const nativeLang of NATIVE_LANGUAGES) {
// Überspringe, wenn Zielsprache = Muttersprache
if (targetLang === nativeLang) continue;
const title = `${targetLang} für ${nativeLang}sprachige - Schnellstart in 4 Wochen`;
let description = `Lerne ${targetLang} schnell und praktisch für den Alltag. `;
if (targetLang === 'Latein') {
description = `Lerne ${targetLang} systematisch mit Fokus auf Grammatik und Vokabular. `;
} else if (targetLang === 'Bisaya') {
description = `Lerne ${targetLang} (Cebuano) schnell und praktisch für den Familienalltag. `;
}
description += `Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.`;
configs.push({
targetLanguageName: targetLang,
nativeLanguageName: nativeLang,
title,
description,
difficultyLevel: 1
});
}
}
return configs;
}
const LANGUAGE_COURSES = generateCourseConfigs();
async function findOrCreateLanguage(languageName, ownerUserId) {
// Suche zuerst nach vorhandener Sprache
const [existing] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = :name LIMIT 1`,
{
replacements: { name: languageName },
type: sequelize.QueryTypes.SELECT
}
);
if (existing) {
console.log(` ✅ Sprache "${languageName}" bereits vorhanden (ID: ${existing.id})`);
return existing.id;
}
// Erstelle neue Sprache
const shareCode = crypto.randomBytes(8).toString('hex');
const [created] = await sequelize.query(
`INSERT INTO community.vocab_language (owner_user_id, name, share_code)
VALUES (:ownerUserId, :name, :shareCode)
RETURNING id`,
{
replacements: { ownerUserId, name: languageName, shareCode },
type: sequelize.QueryTypes.SELECT
}
);
console.log(` ✅ Sprache "${languageName}" erstellt (ID: ${created.id})`);
return created.id;
}
async function createCourseForLanguage(targetLanguageId, nativeLanguageId, languageConfig, ownerUserId) {
const shareCode = crypto.randomBytes(8).toString('hex');
const course = await VocabCourse.create({
ownerUserId,
title: languageConfig.title,
description: languageConfig.description,
languageId: Number(targetLanguageId),
nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null,
difficultyLevel: languageConfig.difficultyLevel || 1,
isPublic: true,
shareCode
});
console.log(` ✅ Kurs erstellt: "${course.title}" (ID: ${course.id}, Share-Code: ${shareCode})`);
// Erstelle Lektionen
const createdLessons = [];
for (const lessonData of LESSON_TEMPLATE) {
const lesson = await VocabCourseLesson.create({
courseId: course.id,
chapterId: null,
lessonNumber: lessonData.num,
title: lessonData.title,
description: lessonData.desc,
weekNumber: lessonData.week,
dayNumber: lessonData.day,
lessonType: lessonData.type,
culturalNotes: lessonData.cultural,
targetMinutes: lessonData.targetMin,
targetScorePercent: lessonData.targetScore,
requiresReview: lessonData.review
});
createdLessons.push({ lesson, lessonData });
}
console.log(`${LESSON_TEMPLATE.length} Lektionen erstellt`);
// Erstelle Beispiel-Grammatik-Übungen für Grammar-Lektionen
let grammarExerciseCount = 0;
for (const { lesson, lessonData } of createdLessons) {
if (lessonData.type === 'grammar') {
// Erstelle 2-3 Beispiel-Übungen für jede Grammar-Lektion
const exercises = createExampleGrammarExercises(lesson.id, lessonData, ownerUserId);
for (const exercise of exercises) {
await VocabGrammarExercise.create(exercise);
grammarExerciseCount++;
}
}
}
if (grammarExerciseCount > 0) {
console.log(`${grammarExerciseCount} Grammatik-Übungen erstellt`);
}
return course;
}
// Erstelle Beispiel-Grammatik-Übungen für eine Grammar-Lektion
function createExampleGrammarExercises(lessonId, lessonData, ownerUserId) {
const exercises = [];
// Beispiel-Übung 1: Gap Fill (Lückentext)
exercises.push({
lessonId: lessonId,
exerciseTypeId: 1, // gap_fill
exerciseNumber: 1,
title: `${lessonData.title} - Übung 1`,
instruction: 'Fülle die Lücken mit den richtigen Wörtern.',
questionData: JSON.stringify({
type: 'gap_fill',
text: 'Hallo! Wie geht es {gap}? Mir geht es {gap}, danke!',
gaps: 2
}),
answerData: JSON.stringify({
type: 'gap_fill',
answers: ['dir', 'gut']
}),
explanation: 'Die richtigen Antworten sind "dir" und "gut".',
createdByUserId: ownerUserId
});
// Beispiel-Übung 2: Multiple Choice
exercises.push({
lessonId: lessonId,
exerciseTypeId: 2, // multiple_choice
exerciseNumber: 2,
title: `${lessonData.title} - Übung 2`,
instruction: 'Wähle die richtige Antwort aus.',
questionData: JSON.stringify({
type: 'multiple_choice',
question: 'Wie sagt man "Guten Tag" auf ' + lessonData.title.split(' - ')[0] + '?',
options: ['Option A', 'Option B', 'Option C', 'Option D']
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: 0
}),
explanation: 'Die richtige Antwort ist Option A.',
createdByUserId: ownerUserId
});
return exercises;
}
async function findOrCreateSystemUser() {
// Versuche zuerst einen System-Benutzer zu finden (z.B. mit username "system" oder "admin")
let systemUser = await User.findOne({
where: {
username: 'system'
}
});
if (!systemUser) {
// Versuche Admin-Benutzer
systemUser = await User.findOne({
where: {
username: 'admin'
}
});
}
if (!systemUser) {
// Erstelle einen System-Benutzer
console.log(' Erstelle System-Benutzer für öffentliche Kurse...');
const saltRounds = 10;
const randomPassword = crypto.randomBytes(32).toString('hex');
const hashedPassword = await bcrypt.hash(randomPassword, saltRounds);
systemUser = await User.create({
username: 'system',
email: 'system@your-part.de',
password: hashedPassword,
active: true,
registrationDate: new Date()
});
// hashedId wird automatisch vom Hook gesetzt, aber warte kurz darauf
await systemUser.reload();
console.log(` ✅ System-Benutzer erstellt (ID: ${systemUser.id}, hashedId: ${systemUser.hashedId})`);
} else {
console.log(` ✅ System-Benutzer gefunden (ID: ${systemUser.id}, Username: ${systemUser.username})`);
}
return systemUser;
}
async function createAllLanguageCourses() {
try {
// Finde oder erstelle System-Benutzer
const systemUser = await findOrCreateSystemUser();
console.log(`\n🚀 Erstelle öffentliche Sprachkurse (Besitzer: System-Benutzer ID ${systemUser.id})\n`);
const createdCourses = [];
// Stelle sicher, dass alle benötigten Sprachen existieren
console.log(`\n🌍 Stelle sicher, dass alle Sprachen existieren...`);
const allLanguages = [...new Set([...TARGET_LANGUAGES, ...NATIVE_LANGUAGES])];
const languageMap = new Map();
for (const langName of allLanguages) {
const langId = await findOrCreateLanguage(langName, systemUser.id);
languageMap.set(langName, langId);
}
for (const langConfig of LANGUAGE_COURSES) {
console.log(`\n📚 Verarbeite: ${langConfig.targetLanguageName} für ${langConfig.nativeLanguageName}sprachige`);
const targetLanguageId = languageMap.get(langConfig.targetLanguageName);
const nativeLanguageId = languageMap.get(langConfig.nativeLanguageName);
// Prüfe, ob Kurs bereits existiert (unabhängig vom Besitzer, wenn öffentlich)
const existingCourse = await VocabCourse.findOne({
where: {
languageId: targetLanguageId,
nativeLanguageId: nativeLanguageId,
isPublic: true
}
});
if (existingCourse) {
console.log(` ⚠️ Kurs "${langConfig.title}" existiert bereits (ID: ${existingCourse.id})`);
createdCourses.push({
...langConfig,
courseId: existingCourse.id,
targetLanguageId,
nativeLanguageId,
skipped: true
});
continue;
}
// Erstelle Kurs
const course = await createCourseForLanguage(targetLanguageId, nativeLanguageId, langConfig, systemUser.id);
createdCourses.push({
...langConfig,
courseId: course.id,
targetLanguageId,
nativeLanguageId,
shareCode: course.shareCode
});
}
console.log(`\n\n🎉 Zusammenfassung:\n`);
console.log(` Gesamt: ${LANGUAGE_COURSES.length} Sprachen`);
console.log(` Erstellt: ${createdCourses.filter(c => !c.skipped).length} Kurse`);
console.log(` Übersprungen: ${createdCourses.filter(c => c.skipped).length} Kurse`);
console.log(`\n📋 Erstellte Kurse:\n`);
for (const course of createdCourses) {
if (course.skipped) {
console.log(` ⚠️ ${course.languageName}: Bereits vorhanden (ID: ${course.courseId})`);
} else {
console.log(`${course.languageName}: ${course.title}`);
console.log(` Share-Code: ${course.shareCode}`);
}
}
console.log(`\n💡 Nächste Schritte:`);
console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`);
console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`);
console.log(` 3. Teile die Kurse mit anderen (Share-Codes oben)`);
return createdCourses;
} catch (error) {
console.error('❌ Fehler beim Erstellen der Kurse:', error);
throw error;
}
}
// CLI-Aufruf
// Keine Parameter mehr nötig - verwendet automatisch System-Benutzer
createAllLanguageCourses()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* Script zum Löschen ALLER "Familienwörter"-Übungen aus Bisaya-Kursen
*
* Verwendung:
* node backend/scripts/delete-all-family-words-exercises.js
*
* Löscht alle Grammatik-Übungen von "Familienwörter"-Lektionen, um Platz für neue zu schaffen.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
async function deleteAllFamilyWordsExercises() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
// Finde alle Bisaya-Kurse
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const courses = await sequelize.query(
`SELECT id, title FROM community.vocab_course WHERE language_id = :languageId`,
{
replacements: { languageId: bisayaLanguage.id },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
let totalDeleted = 0;
let totalLessons = 0;
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
// Finde "Familienwörter"-Lektionen
const lessons = await VocabCourseLesson.findAll({
where: {
courseId: course.id,
title: 'Familienwörter'
},
order: [['lessonNumber', 'ASC']]
});
console.log(` ${lessons.length} "Familienwörter"-Lektionen gefunden`);
for (const lesson of lessons) {
// Lösche ALLE bestehenden Übungen
const deletedCount = await VocabGrammarExercise.destroy({
where: { lessonId: lesson.id }
});
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} Übung(en) gelöscht`);
totalDeleted += deletedCount;
totalLessons++;
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalLessons} Lektionen bearbeitet`);
console.log(` ${totalDeleted} Übungen gelöscht`);
console.log(`\n💡 Hinweis: Führe jetzt das update-family-words-exercises.js Script aus, um neue Übungen zu erstellen.`);
}
deleteAllFamilyWordsExercises()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* Script zum Korrigieren der Gap-Fill-Übung in "Begrüßungen & Höflichkeit"
* Fügt Muttersprache-Hinweise hinzu, damit Vokabeln korrekt extrahiert werden können
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import VocabCourse from '../models/community/vocab_course.js';
async function fixBegruessungenGapFill() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
// Finde alle "Begrüßungen & Höflichkeit" Lektionen
const lessons = await VocabCourseLesson.findAll({
where: { title: 'Begrüßungen & Höflichkeit' },
include: [{ model: VocabCourse, as: 'course' }]
});
console.log(`Gefunden: ${lessons.length} "Begrüßungen & Höflichkeit"-Lektionen\n`);
let totalUpdated = 0;
for (const lesson of lessons) {
console.log(`📚 Kurs: ${lesson.course.title} (Kurs-ID: ${lesson.course.id}, Lektion-ID: ${lesson.id})`);
// Finde Gap-Fill-Übung mit "ko" als Antwort
const exercises = await VocabGrammarExercise.findAll({
where: {
lessonId: lesson.id,
exerciseTypeId: 1 // gap_fill
}
});
for (const exercise of exercises) {
const qData = typeof exercise.questionData === 'string'
? JSON.parse(exercise.questionData)
: exercise.questionData;
const aData = typeof exercise.answerData === 'string'
? JSON.parse(exercise.answerData)
: exercise.answerData;
// Prüfe ob es die problematische Übung ist (enthält "ko" als Antwort ohne Muttersprache-Hinweis)
if (aData.answers && aData.answers.includes('ko')) {
const text = qData.text || '';
// Prüfe ob Muttersprache-Hinweise fehlen
if (!text.includes('(ich)') && !text.includes('(I)')) {
console.log(` 🔧 Korrigiere Übung "${exercise.title}" (ID: ${exercise.id})`);
// Korrigiere den Text
const correctedText = text.replace(
'Maayo {gap}.',
'Maayo {gap} (ich).'
);
qData.text = correctedText;
await exercise.update({
questionData: qData
});
totalUpdated++;
console.log(` ✅ Aktualisiert: "${correctedText}"`);
} else {
console.log(` ✓ Übung "${exercise.title}" bereits korrekt`);
}
}
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${lessons.length} Lektionen verarbeitet`);
console.log(` ${totalUpdated} Übungen aktualisiert`);
}
fixBegruessungenGapFill()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,552 @@
#!/usr/bin/env node
/**
* Script zum Erstellen von Übungen für die "Familien-Gespräche" Lektion
*
* Verwendung:
* node backend/scripts/update-family-conversations-exercises.js
*
* Erstellt Gesprächsübungen für die "Familien-Gespräche" Lektion in allen Bisaya-Kursen.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import VocabCourse from '../models/community/vocab_course.js';
import User from '../models/community/user.js';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { Op } from 'sequelize';
// Familiengespräche auf Bisaya mit verschiedenen Muttersprachen
const FAMILY_CONVERSATIONS = {
// Deutsch -> Bisaya
'Deutsch': {
conversations: [
{
bisaya: 'Kumusta ka, Nanay?',
native: 'Wie geht es dir, Mama?',
explanation: '"Kumusta ka" ist "Wie geht es dir?" und "Nanay" ist "Mama"'
},
{
bisaya: 'Maayo ko, Salamat. Ikaw?',
native: 'Mir geht es gut, danke. Und dir?',
explanation: '"Maayo ko" bedeutet "Mir geht es gut", "Ikaw" ist "du" (formal)'
},
{
bisaya: 'Asa si Tatay?',
native: 'Wo ist Papa?',
explanation: '"Asa" bedeutet "wo", "si" ist ein Marker für Personen, "Tatay" ist "Papa"'
},
{
bisaya: 'Naa siya sa balay.',
native: 'Er ist zu Hause.',
explanation: '"Naa" bedeutet "ist/sein", "siya" ist "er/sie", "sa balay" ist "zu Hause"'
},
{
bisaya: 'Kumusta na ang Kuya?',
native: 'Wie geht es dem älteren Bruder?',
explanation: '"Kumusta na" ist "Wie geht es", "ang" ist ein Artikel, "Kuya" ist "älterer Bruder"'
},
{
bisaya: 'Maayo ra siya.',
native: 'Es geht ihm gut.',
explanation: '"Maayo ra" bedeutet "gut/gut geht es", "siya" ist "ihm"'
},
{
bisaya: 'Gutom na ko, Nanay.',
native: 'Ich bin hungrig, Mama.',
explanation: '"Gutom" bedeutet "hungrig", "na" zeigt einen Zustand, "ko" ist "ich"'
},
{
bisaya: 'Hulata lang, hapit na ang pagkaon.',
native: 'Warte nur, das Essen ist fast fertig.',
explanation: '"Hulata" ist "warte", "lang" ist "nur", "hapit na" ist "fast", "pagkaon" ist "Essen"'
}
]
},
// Englisch -> Bisaya
'Englisch': {
conversations: [
{
bisaya: 'Kumusta ka, Nanay?',
native: 'How are you, Mom?',
explanation: '"Kumusta ka" means "How are you?" and "Nanay" means "Mom"'
},
{
bisaya: 'Maayo ko, Salamat. Ikaw?',
native: 'I\'m fine, thank you. And you?',
explanation: '"Maayo ko" means "I\'m fine", "Ikaw" is "you" (formal)'
},
{
bisaya: 'Asa si Tatay?',
native: 'Where is Dad?',
explanation: '"Asa" means "where", "si" is a person marker, "Tatay" means "Dad"'
},
{
bisaya: 'Naa siya sa balay.',
native: 'He is at home.',
explanation: '"Naa" means "is/be", "siya" is "he/she", "sa balay" means "at home"'
},
{
bisaya: 'Kumusta na ang Kuya?',
native: 'How is the older brother?',
explanation: '"Kumusta na" means "How is", "ang" is an article, "Kuya" means "older brother"'
},
{
bisaya: 'Maayo ra siya.',
native: 'He is fine.',
explanation: '"Maayo ra" means "fine/well", "siya" means "he"'
},
{
bisaya: 'Gutom na ko, Nanay.',
native: 'I\'m hungry, Mom.',
explanation: '"Gutom" means "hungry", "na" shows a state, "ko" is "I"'
},
{
bisaya: 'Hulata lang, hapit na ang pagkaon.',
native: 'Just wait, the food is almost ready.',
explanation: '"Hulata" means "wait", "lang" means "just", "hapit na" means "almost", "pagkaon" means "food"'
}
]
},
// Spanisch -> Bisaya
'Spanisch': {
conversations: [
{
bisaya: 'Kumusta ka, Nanay?',
native: '¿Cómo estás, Mamá?',
explanation: '"Kumusta ka" significa "¿Cómo estás?" y "Nanay" significa "Mamá"'
},
{
bisaya: 'Maayo ko, Salamat. Ikaw?',
native: 'Estoy bien, gracias. ¿Y tú?',
explanation: '"Maayo ko" significa "Estoy bien", "Ikaw" es "tú" (formal)'
},
{
bisaya: 'Asa si Tatay?',
native: '¿Dónde está Papá?',
explanation: '"Asa" significa "dónde", "si" es un marcador de persona, "Tatay" significa "Papá"'
},
{
bisaya: 'Naa siya sa balay.',
native: 'Él está en casa.',
explanation: '"Naa" significa "está/ser", "siya" es "él/ella", "sa balay" significa "en casa"'
},
{
bisaya: 'Kumusta na ang Kuya?',
native: '¿Cómo está el hermano mayor?',
explanation: '"Kumusta na" significa "¿Cómo está?", "ang" es un artículo, "Kuya" significa "hermano mayor"'
},
{
bisaya: 'Maayo ra siya.',
native: 'Él está bien.',
explanation: '"Maayo ra" significa "bien", "siya" significa "él"'
},
{
bisaya: 'Gutom na ko, Nanay.',
native: 'Tengo hambre, Mamá.',
explanation: '"Gutom" significa "hambriento", "na" muestra un estado, "ko" es "yo"'
},
{
bisaya: 'Hulata lang, hapit na ang pagkaon.',
native: 'Solo espera, la comida está casi lista.',
explanation: '"Hulata" significa "espera", "lang" significa "solo", "hapit na" significa "casi", "pagkaon" significa "comida"'
}
]
},
// Französisch -> Bisaya
'Französisch': {
conversations: [
{
bisaya: 'Kumusta ka, Nanay?',
native: 'Comment vas-tu, Maman?',
explanation: '"Kumusta ka" signifie "Comment vas-tu?" et "Nanay" signifie "Maman"'
},
{
bisaya: 'Maayo ko, Salamat. Ikaw?',
native: 'Je vais bien, merci. Et toi?',
explanation: '"Maayo ko" signifie "Je vais bien", "Ikaw" est "tu" (formel)'
},
{
bisaya: 'Asa si Tatay?',
native: 'Où est Papa?',
explanation: '"Asa" signifie "où", "si" est un marqueur de personne, "Tatay" signifie "Papa"'
},
{
bisaya: 'Naa siya sa balay.',
native: 'Il est à la maison.',
explanation: '"Naa" signifie "est/être", "siya" est "il/elle", "sa balay" signifie "à la maison"'
},
{
bisaya: 'Kumusta na ang Kuya?',
native: 'Comment va le grand frère?',
explanation: '"Kumusta na" signifie "Comment va", "ang" est un article, "Kuya" signifie "grand frère"'
},
{
bisaya: 'Maayo ra siya.',
native: 'Il va bien.',
explanation: '"Maayo ra" signifie "bien", "siya" signifie "il"'
},
{
bisaya: 'Gutom na ko, Nanay.',
native: 'J\'ai faim, Maman.',
explanation: '"Gutom" signifie "faim", "na" montre un état, "ko" est "je"'
},
{
bisaya: 'Hulata lang, hapit na ang pagkaon.',
native: 'Attends juste, la nourriture est presque prête.',
explanation: '"Hulata" signifie "attends", "lang" signifie "juste", "hapit na" signifie "presque", "pagkaon" signifie "nourriture"'
}
]
},
// Italienisch -> Bisaya
'Italienisch': {
conversations: [
{
bisaya: 'Kumusta ka, Nanay?',
native: 'Come stai, Mamma?',
explanation: '"Kumusta ka" significa "Come stai?" e "Nanay" significa "Mamma"'
},
{
bisaya: 'Maayo ko, Salamat. Ikaw?',
native: 'Sto bene, grazie. E tu?',
explanation: '"Maayo ko" significa "Sto bene", "Ikaw" è "tu" (formale)'
},
{
bisaya: 'Asa si Tatay?',
native: 'Dove è Papà?',
explanation: '"Asa" significa "dove", "si" è un marcatore di persona, "Tatay" significa "Papà"'
},
{
bisaya: 'Naa siya sa balay.',
native: 'È a casa.',
explanation: '"Naa" significa "è/essere", "siya" è "lui/lei", "sa balay" significa "a casa"'
},
{
bisaya: 'Kumusta na ang Kuya?',
native: 'Come sta il fratello maggiore?',
explanation: '"Kumusta na" significa "Come sta", "ang" è un articolo, "Kuya" significa "fratello maggiore"'
},
{
bisaya: 'Maayo ra siya.',
native: 'Sta bene.',
explanation: '"Maayo ra" significa "bene", "siya" significa "lui"'
},
{
bisaya: 'Gutom na ko, Nanay.',
native: 'Ho fame, Mamma.',
explanation: '"Gutom" significa "fame", "na" mostra uno stato, "ko" è "io"'
},
{
bisaya: 'Hulata lang, hapit na ang pagkaon.',
native: 'Aspetta solo, il cibo è quasi pronto.',
explanation: '"Hulata" significa "aspetta", "lang" significa "solo", "hapit na" significa "quasi", "pagkaon" significa "cibo"'
}
]
},
// Portugiesisch -> Bisaya
'Portugiesisch': {
conversations: [
{
bisaya: 'Kumusta ka, Nanay?',
native: 'Como você está, Mãe?',
explanation: '"Kumusta ka" significa "Como você está?" e "Nanay" significa "Mãe"'
},
{
bisaya: 'Maayo ko, Salamat. Ikaw?',
native: 'Estou bem, obrigado. E você?',
explanation: '"Maayo ko" significa "Estou bem", "Ikaw" é "você" (formal)'
},
{
bisaya: 'Asa si Tatay?',
native: 'Onde está o Papai?',
explanation: '"Asa" significa "onde", "si" é um marcador de pessoa, "Tatay" significa "Papai"'
},
{
bisaya: 'Naa siya sa balay.',
native: 'Ele está em casa.',
explanation: '"Naa" significa "está/ser", "siya" é "ele/ela", "sa balay" significa "em casa"'
},
{
bisaya: 'Kumusta na ang Kuya?',
native: 'Como está o irmão mais velho?',
explanation: '"Kumusta na" significa "Como está", "ang" é um artigo, "Kuya" significa "irmão mais velho"'
},
{
bisaya: 'Maayo ra siya.',
native: 'Ele está bem.',
explanation: '"Maayo ra" significa "bem", "siya" significa "ele"'
},
{
bisaya: 'Gutom na ko, Nanay.',
native: 'Estou com fome, Mãe.',
explanation: '"Gutom" significa "fome", "na" mostra um estado, "ko" é "eu"'
},
{
bisaya: 'Hulata lang, hapit na ang pagkaon.',
native: 'Apenas espere, a comida está quase pronta.',
explanation: '"Hulata" significa "espere", "lang" significa "apenas", "hapit na" significa "quase", "pagkaon" significa "comida"'
}
]
}
};
async function findOrCreateSystemUser() {
// Versuche zuerst einen System-Benutzer zu finden (z.B. mit username "system" oder "admin")
let systemUser = await User.findOne({
where: {
username: { [sequelize.Sequelize.Op.in]: ['system', 'admin', 'System', 'Admin'] }
}
});
if (!systemUser) {
// Erstelle einen System-Benutzer
const password = crypto.randomBytes(32).toString('hex');
const hashedPassword = await bcrypt.hash(password, 10);
const hashedId = crypto.createHash('sha256').update(`system-${Date.now()}`).digest('hex');
systemUser = await User.create({
username: 'system',
password: hashedPassword,
hashedId: hashedId,
email: 'system@your-part.de'
});
console.log('✅ System-Benutzer erstellt:', systemUser.hashedId);
} else {
console.log('✅ System-Benutzer gefunden:', systemUser.hashedId);
}
return systemUser;
}
function createFamilyConversationExercises(nativeLanguageName) {
const exercises = [];
const conversations = FAMILY_CONVERSATIONS[nativeLanguageName]?.conversations || [];
if (conversations.length === 0) {
console.warn(`⚠️ Keine Gespräche für Muttersprache "${nativeLanguageName}" gefunden. Verwende Deutsch als Fallback.`);
return createFamilyConversationExercises('Deutsch');
}
let exerciseNum = 1;
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche)
conversations.forEach((conv, idx) => {
// Erstelle für jedes Gespräch eine Multiple Choice Übung
const wrongOptions = conversations
.filter((c, i) => i !== idx)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map(c => c.native);
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
const correctIndex = options.indexOf(conv.native);
exercises.push({
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNum++,
title: `Familien-Gespräch ${idx + 1} - Übersetzung`,
instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName,
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`,
options: options
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: correctIndex
}),
explanation: conv.explanation
});
});
// Multiple Choice: Rückwärts-Übersetzung (Was bedeutet dieser Satz?)
conversations.forEach((conv, idx) => {
if (idx < 6) { // Erste 6 als Rückwärts-Übersetzung
const wrongOptions = conversations
.filter((c, i) => i !== idx)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map(c => c.native);
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
const correctIndex = options.indexOf(conv.native);
exercises.push({
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNum++,
title: `Familien-Gespräch ${idx + 1} - Was bedeutet dieser Satz?`,
instruction: 'Was bedeutet dieser Bisaya-Satz?',
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Was bedeutet "${conv.bisaya}"?`,
options: options
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: correctIndex
}),
explanation: conv.explanation
});
}
});
// Gap Fill: Vervollständige Familiengespräche (mehrere Varianten)
exercises.push({
exerciseTypeId: 1, // gap_fill
exerciseNumber: exerciseNum++,
title: 'Familien-Gespräch 1 - Vervollständigen',
instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.',
questionData: JSON.stringify({
type: 'gap_fill',
text: 'Person A: Kumusta ka, {gap}? (Mama)\nPerson B: {gap} ko, Salamat. Ikaw? (Mir geht es gut)',
gaps: 2
}),
answerData: JSON.stringify({
type: 'gap_fill',
answers: ['Nanay', 'Maayo']
}),
explanation: '"Nanay" ist "Mama" und "Maayo ko" bedeutet "Mir geht es gut"'
});
exercises.push({
exerciseTypeId: 1, // gap_fill
exerciseNumber: exerciseNum++,
title: 'Familien-Gespräch 2 - Vervollständigen',
instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.',
questionData: JSON.stringify({
type: 'gap_fill',
text: 'Person A: {gap} si Tatay? (Wo ist)\nPerson B: {gap} siya sa balay. (Er ist)',
gaps: 2
}),
answerData: JSON.stringify({
type: 'gap_fill',
answers: ['Asa', 'Naa']
}),
explanation: '"Asa" bedeutet "wo" und "Naa" bedeutet "ist/sein"'
});
// Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten)
conversations.slice(0, 4).forEach((conv, idx) => {
exercises.push({
exerciseTypeId: 3, // transformation
exerciseNumber: exerciseNum++,
title: `Familien-Gespräch ${idx + 1} - Übersetzung nach Bisaya`,
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: JSON.stringify({
type: 'transformation',
text: conv.native
}),
answerData: JSON.stringify({
type: 'transformation',
correctAnswer: conv.bisaya
}),
explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}`
});
});
return exercises;
}
async function updateFamilyConversationExercises() {
await sequelize.authenticate();
console.log('✅ Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
// Finde Bisaya-Sprache mit SQL
const [bisayaLangResult] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!bisayaLangResult) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const bisayaLanguageId = bisayaLangResult.id;
// Hole alle Bisaya-Kurse mit native language info
const courses = await sequelize.query(
`SELECT
c.id,
c.title,
c.native_language_id,
nl.name as native_language_name
FROM community.vocab_course c
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
WHERE c.language_id = :bisayaLanguageId`,
{
replacements: { bisayaLanguageId },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`📚 Gefunden: ${courses.length} Bisaya-Kurse\n`);
let totalExercisesCreated = 0;
let totalLessonsProcessed = 0;
for (const course of courses) {
console.log(`📖 Kurs: ${course.title} (ID: ${course.id})`);
// Finde native language name
const nativeLanguageName = course.native_language_name || 'Deutsch';
console.log(` Muttersprache: ${nativeLanguageName}`);
// Finde "Familien-Gespräche" Lektion
const lessons = await VocabCourseLesson.findAll({
where: {
courseId: course.id,
title: 'Familien-Gespräche'
},
attributes: ['id', 'title', 'lessonNumber']
});
console.log(` ${lessons.length} "Familien-Gespräche"-Lektion(en) gefunden`);
for (const lesson of lessons) {
// Lösche vorhandene Übungen
const deletedCount = await VocabGrammarExercise.destroy({
where: { lessonId: lesson.id }
});
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
// Erstelle neue Übungen
const exercises = createFamilyConversationExercises(nativeLanguageName);
if (exercises.length > 0) {
const exercisesToCreate = exercises.map(ex => ({
...ex,
lessonId: lesson.id,
createdByUserId: systemUser.id
}));
await VocabGrammarExercise.bulkCreate(exercisesToCreate);
totalExercisesCreated += exercisesToCreate.length;
console.log(`${exercisesToCreate.length} neue Übung(en) erstellt`);
} else {
console.log(` ⚠️ Keine Übungen erstellt`);
}
totalLessonsProcessed++;
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalLessonsProcessed} "Familien-Gespräche"-Lektion(en) verarbeitet`);
console.log(` ${totalExercisesCreated} Grammatik-Übungen erstellt`);
}
updateFamilyConversationExercises()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env node
/**
* Script zum Aktualisieren der "Familienwörter"-Übungen in Bisaya-Kursen
*
* Verwendung:
* node backend/scripts/update-family-words-exercises.js
*
* Ersetzt bestehende Dummy-Übungen durch spezifische Familienwörter-Übungen.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import VocabCourse from '../models/community/vocab_course.js';
import User from '../models/community/user.js';
// Familienwörter-Übersetzungen in verschiedene Muttersprachen
const FAMILY_WORDS = {
Mutter: {
de: 'Mutter',
en: 'Mother',
es: 'Madre',
fr: 'Mère',
it: 'Madre',
pt: 'Mãe'
},
Vater: {
de: 'Vater',
en: 'Father',
es: 'Padre',
fr: 'Père',
it: 'Padre',
pt: 'Pai'
},
'älterer Bruder': {
de: 'älterer Bruder',
en: 'older brother',
es: 'hermano mayor',
fr: 'frère aîné',
it: 'fratello maggiore',
pt: 'irmão mais velho'
},
'ältere Schwester': {
de: 'ältere Schwester',
en: 'older sister',
es: 'hermana mayor',
fr: 'sœur aînée',
it: 'sorella maggiore',
pt: 'irmã mais velha'
},
Großmutter: {
de: 'Großmutter',
en: 'Grandmother',
es: 'Abuela',
fr: 'Grand-mère',
it: 'Nonna',
pt: 'Avó'
},
Großvater: {
de: 'Großvater',
en: 'Grandfather',
es: 'Abuelo',
fr: 'Grand-père',
it: 'Nonno',
pt: 'Avô'
}
};
// Bisaya-Übersetzungen
const BISAYA_TRANSLATIONS = {
'Mutter': 'Nanay',
'Vater': 'Tatay',
'älterer Bruder': 'Kuya',
'ältere Schwester': 'Ate',
'Großmutter': 'Lola',
'Großvater': 'Lolo'
};
// Sprach-Codes für Mapping
const LANGUAGE_CODES = {
'Deutsch': 'de',
'English': 'en',
'Español': 'es',
'Français': 'fr',
'Italiano': 'it',
'Português': 'pt',
'Spanish': 'es',
'French': 'fr',
'Italian': 'it',
'Portuguese': 'pt'
};
// Erstelle Übungen basierend auf Muttersprache
function createFamilyWordsExercises(nativeLanguageName) {
const langCode = LANGUAGE_CODES[nativeLanguageName] || 'de'; // Fallback zu Deutsch
const exercises = [];
// Multiple Choice Übungen für jedes Familienwort
const familyWords = Object.keys(FAMILY_WORDS);
const bisayaWords = ['Nanay', 'Tatay', 'Kuya', 'Ate', 'Lola', 'Lolo'];
familyWords.forEach((key, index) => {
const nativeWord = FAMILY_WORDS[key][langCode];
const bisayaWord = BISAYA_TRANSLATIONS[key];
// Erstelle Multiple Choice mit falschen Antworten
const wrongAnswers = bisayaWords.filter(w => w !== bisayaWord);
const shuffledWrong = wrongAnswers.sort(() => Math.random() - 0.5).slice(0, 3);
const options = [bisayaWord, ...shuffledWrong].sort(() => Math.random() - 0.5);
const correctIndex = options.indexOf(bisayaWord);
exercises.push({
exerciseTypeId: 2, // multiple_choice
title: `Wie sagt man "${nativeWord}" auf Bisaya?`,
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: `Wie sagt man "${nativeWord}" auf Bisaya?`,
options: options
},
answerData: {
type: 'multiple_choice',
correctAnswer: correctIndex
},
explanation: `"${bisayaWord}" bedeutet "${nativeWord}" auf Bisaya.`
});
});
// Gap Fill Übung
const nativeWords = familyWords.map(key => FAMILY_WORDS[key][langCode]);
exercises.push({
exerciseTypeId: 1, // gap_fill
title: 'Familienwörter vervollständigen',
instruction: `Fülle die Lücken mit den richtigen Bisaya-Familienwörtern.`,
questionData: {
type: 'gap_fill',
text: familyWords.map((key, i) => `{gap} (${nativeWords[i]})`).join(' | '),
gaps: familyWords.length
},
answerData: {
type: 'gap_fill',
answers: bisayaWords
},
explanation: bisayaWords.map((bw, i) => `${bw} = ${nativeWords[i]}`).join(', ') + '.'
});
// Transformation Übung
exercises.push({
exerciseTypeId: 4, // transformation
title: 'Familienwörter übersetzen',
instruction: `Übersetze das Familienwort ins Bisaya.`,
questionData: {
type: 'transformation',
text: nativeWords[0], // Erste Muttersprache als Beispiel
sourceLanguage: nativeLanguageName || 'Deutsch',
targetLanguage: 'Bisaya'
},
answerData: {
type: 'transformation',
correct: bisayaWords[0],
alternatives: [bisayaWords[0]] // Nur die korrekte Antwort
},
explanation: `"${bisayaWords[0]}" bedeutet "${nativeWords[0]}" auf Bisaya.`
});
return exercises;
}
async function findOrCreateSystemUser() {
let systemUser = await User.findOne({
where: {
username: 'system'
}
});
if (!systemUser) {
systemUser = await User.findOne({
where: {
username: 'admin'
}
});
}
if (!systemUser) {
console.error('❌ System-Benutzer nicht gefunden.');
throw new Error('System user not found');
}
return systemUser;
}
async function updateFamilyWordsExercises() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
// Finde alle Bisaya-Kurse
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const courses = await sequelize.query(
`SELECT c.id, c.title, c.owner_user_id, c.native_language_id, nl.name as native_language_name
FROM community.vocab_course c
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
WHERE c.language_id = :languageId`,
{
replacements: { languageId: bisayaLanguage.id },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
let totalExercisesUpdated = 0;
let totalLessonsUpdated = 0;
for (const course of courses) {
const nativeLangName = course.native_language_name || 'Deutsch'; // Fallback zu Deutsch
console.log(`📚 Kurs: ${course.title} (ID: ${course.id}, Muttersprache: ${nativeLangName})`);
// Erstelle Übungen für diese Muttersprache
const exercises = createFamilyWordsExercises(nativeLangName);
// Finde "Familienwörter"-Lektionen
const lessons = await VocabCourseLesson.findAll({
where: {
courseId: course.id,
title: 'Familienwörter'
},
order: [['lessonNumber', 'ASC']]
});
console.log(` ${lessons.length} "Familienwörter"-Lektionen gefunden\n`);
for (const lesson of lessons) {
// Lösche bestehende Übungen (inkl. Dummy-Übungen)
const deletedCount = await VocabGrammarExercise.destroy({
where: { lessonId: lesson.id }
});
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
// Erstelle neue Übungen
let exerciseNumber = 1;
for (const exerciseData of exercises) {
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: exerciseData.exerciseTypeId,
exerciseNumber: exerciseNumber++,
title: exerciseData.title,
instruction: exerciseData.instruction,
questionData: JSON.stringify(exerciseData.questionData),
answerData: JSON.stringify(exerciseData.answerData),
explanation: exerciseData.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesUpdated++;
}
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} neue Übung(en) erstellt (${nativeLangName} → Bisaya)`);
totalLessonsUpdated++;
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalLessonsUpdated} Lektionen aktualisiert`);
console.log(` ${totalExercisesUpdated} neue Grammatik-Übungen erstellt`);
}
updateFamilyWordsExercises()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,552 @@
#!/usr/bin/env node
/**
* Script zum Erstellen von Übungen für die "Gefühle & Zuneigung" Lektion
*
* Verwendung:
* node backend/scripts/update-feelings-affection-exercises.js
*
* Erstellt Gesprächsübungen für die "Gefühle & Zuneigung" Lektion in allen Bisaya-Kursen.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import VocabCourse from '../models/community/vocab_course.js';
import User from '../models/community/user.js';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { Op } from 'sequelize';
// Gefühle & Zuneigung auf Bisaya mit verschiedenen Muttersprachen
const FEELINGS_AFFECTION = {
// Deutsch -> Bisaya
'Deutsch': {
conversations: [
{
bisaya: 'Gihigugma ko ikaw.',
native: 'Ich liebe dich.',
explanation: '"Gihigugma" bedeutet "lieben", "ko" ist "ich", "ikaw" ist "dich"'
},
{
bisaya: 'Nahigugma ko nimo.',
native: 'Ich liebe dich. (alternativ)',
explanation: '"Nahigugma" ist eine andere Form von "lieben", "nimo" ist "dich" (informell)'
},
{
bisaya: 'Ganahan ko nimo.',
native: 'Ich mag dich.',
explanation: '"Ganahan" bedeutet "mögen/gefallen", "ko" ist "ich", "nimo" ist "dich"'
},
{
bisaya: 'Nalipay ko nga nakita ka.',
native: 'Ich bin glücklich, dich zu sehen.',
explanation: '"Nalipay" bedeutet "glücklich", "ko" ist "ich", "nga nakita ka" ist "dich zu sehen"'
},
{
bisaya: 'Gimingaw ko nimo.',
native: 'Ich vermisse dich.',
explanation: '"Gimingaw" bedeutet "vermissen", "ko" ist "ich", "nimo" ist "dich"'
},
{
bisaya: 'Nalipay ko.',
native: 'Ich bin glücklich.',
explanation: '"Nalipay" bedeutet "glücklich", "ko" ist "ich"'
},
{
bisaya: 'Nasubo ko.',
native: 'Ich bin traurig.',
explanation: '"Nasubo" bedeutet "traurig", "ko" ist "ich"'
},
{
bisaya: 'Nalipay ko nga naa ka dinhi.',
native: 'Ich bin glücklich, dass du hier bist.',
explanation: '"Nalipay" ist "glücklich", "nga naa ka dinhi" bedeutet "dass du hier bist"'
}
]
},
// Englisch -> Bisaya
'Englisch': {
conversations: [
{
bisaya: 'Gihigugma ko ikaw.',
native: 'I love you.',
explanation: '"Gihigugma" means "love", "ko" is "I", "ikaw" is "you"'
},
{
bisaya: 'Nahigugma ko nimo.',
native: 'I love you. (alternative)',
explanation: '"Nahigugma" is another form of "love", "nimo" is "you" (informal)'
},
{
bisaya: 'Ganahan ko nimo.',
native: 'I like you.',
explanation: '"Ganahan" means "like", "ko" is "I", "nimo" is "you"'
},
{
bisaya: 'Nalipay ko nga nakita ka.',
native: 'I am happy to see you.',
explanation: '"Nalipay" means "happy", "ko" is "I", "nga nakita ka" is "to see you"'
},
{
bisaya: 'Gimingaw ko nimo.',
native: 'I miss you.',
explanation: '"Gimingaw" means "miss", "ko" is "I", "nimo" is "you"'
},
{
bisaya: 'Nalipay ko.',
native: 'I am happy.',
explanation: '"Nalipay" means "happy", "ko" is "I"'
},
{
bisaya: 'Nasubo ko.',
native: 'I am sad.',
explanation: '"Nasubo" means "sad", "ko" is "I"'
},
{
bisaya: 'Nalipay ko nga naa ka dinhi.',
native: 'I am happy that you are here.',
explanation: '"Nalipay" is "happy", "nga naa ka dinhi" means "that you are here"'
}
]
},
// Spanisch -> Bisaya
'Spanisch': {
conversations: [
{
bisaya: 'Gihigugma ko ikaw.',
native: 'Te amo.',
explanation: '"Gihigugma" significa "amar", "ko" es "yo", "ikaw" es "tú"'
},
{
bisaya: 'Nahigugma ko nimo.',
native: 'Te amo. (alternativa)',
explanation: '"Nahigugma" es otra forma de "amar", "nimo" es "tú" (informal)'
},
{
bisaya: 'Ganahan ko nimo.',
native: 'Me gustas.',
explanation: '"Ganahan" significa "gustar", "ko" es "yo", "nimo" es "tú"'
},
{
bisaya: 'Nalipay ko nga nakita ka.',
native: 'Estoy feliz de verte.',
explanation: '"Nalipay" significa "feliz", "ko" es "yo", "nga nakita ka" es "verte"'
},
{
bisaya: 'Gimingaw ko nimo.',
native: 'Te extraño.',
explanation: '"Gimingaw" significa "extrañar", "ko" es "yo", "nimo" es "tú"'
},
{
bisaya: 'Nalipay ko.',
native: 'Estoy feliz.',
explanation: '"Nalipay" significa "feliz", "ko" es "yo"'
},
{
bisaya: 'Nasubo ko.',
native: 'Estoy triste.',
explanation: '"Nasubo" significa "triste", "ko" es "yo"'
},
{
bisaya: 'Nalipay ko nga naa ka dinhi.',
native: 'Estoy feliz de que estés aquí.',
explanation: '"Nalipay" es "feliz", "nga naa ka dinhi" significa "que estés aquí"'
}
]
},
// Französisch -> Bisaya
'Französisch': {
conversations: [
{
bisaya: 'Gihigugma ko ikaw.',
native: 'Je t\'aime.',
explanation: '"Gihigugma" signifie "aimer", "ko" est "je", "ikaw" est "tu"'
},
{
bisaya: 'Nahigugma ko nimo.',
native: 'Je t\'aime. (alternative)',
explanation: '"Nahigugma" est une autre forme de "aimer", "nimo" est "tu" (informel)'
},
{
bisaya: 'Ganahan ko nimo.',
native: 'Je t\'aime bien.',
explanation: '"Ganahan" signifie "aimer bien", "ko" est "je", "nimo" est "tu"'
},
{
bisaya: 'Nalipay ko nga nakita ka.',
native: 'Je suis heureux de te voir.',
explanation: '"Nalipay" signifie "heureux", "ko" est "je", "nga nakita ka" est "te voir"'
},
{
bisaya: 'Gimingaw ko nimo.',
native: 'Tu me manques.',
explanation: '"Gimingaw" signifie "manquer", "ko" est "je", "nimo" est "tu"'
},
{
bisaya: 'Nalipay ko.',
native: 'Je suis heureux.',
explanation: '"Nalipay" signifie "heureux", "ko" est "je"'
},
{
bisaya: 'Nasubo ko.',
native: 'Je suis triste.',
explanation: '"Nasubo" signifie "triste", "ko" est "je"'
},
{
bisaya: 'Nalipay ko nga naa ka dinhi.',
native: 'Je suis heureux que tu sois ici.',
explanation: '"Nalipay" est "heureux", "nga naa ka dinhi" signifie "que tu sois ici"'
}
]
},
// Italienisch -> Bisaya
'Italienisch': {
conversations: [
{
bisaya: 'Gihigugma ko ikaw.',
native: 'Ti amo.',
explanation: '"Gihigugma" significa "amare", "ko" è "io", "ikaw" è "tu"'
},
{
bisaya: 'Nahigugma ko nimo.',
native: 'Ti amo. (alternativa)',
explanation: '"Nahigugma" è un\'altra forma di "amare", "nimo" è "tu" (informale)'
},
{
bisaya: 'Ganahan ko nimo.',
native: 'Mi piaci.',
explanation: '"Ganahan" significa "piacere", "ko" è "io", "nimo" è "tu"'
},
{
bisaya: 'Nalipay ko nga nakita ka.',
native: 'Sono felice di vederti.',
explanation: '"Nalipay" significa "felice", "ko" è "io", "nga nakita ka" è "vederti"'
},
{
bisaya: 'Gimingaw ko nimo.',
native: 'Mi manchi.',
explanation: '"Gimingaw" significa "mancare", "ko" è "io", "nimo" è "tu"'
},
{
bisaya: 'Nalipay ko.',
native: 'Sono felice.',
explanation: '"Nalipay" significa "felice", "ko" è "io"'
},
{
bisaya: 'Nasubo ko.',
native: 'Sono triste.',
explanation: '"Nasubo" significa "triste", "ko" è "io"'
},
{
bisaya: 'Nalipay ko nga naa ka dinhi.',
native: 'Sono felice che tu sia qui.',
explanation: '"Nalipay" è "felice", "nga naa ka dinhi" significa "che tu sia qui"'
}
]
},
// Portugiesisch -> Bisaya
'Portugiesisch': {
conversations: [
{
bisaya: 'Gihigugma ko ikaw.',
native: 'Eu te amo.',
explanation: '"Gihigugma" significa "amar", "ko" é "eu", "ikaw" é "você"'
},
{
bisaya: 'Nahigugma ko nimo.',
native: 'Eu te amo. (alternativa)',
explanation: '"Nahigugma" é outra forma de "amar", "nimo" é "você" (informal)'
},
{
bisaya: 'Ganahan ko nimo.',
native: 'Eu gosto de você.',
explanation: '"Ganahan" significa "gostar", "ko" é "eu", "nimo" é "você"'
},
{
bisaya: 'Nalipay ko nga nakita ka.',
native: 'Estou feliz em te ver.',
explanation: '"Nalipay" significa "feliz", "ko" é "eu", "nga nakita ka" é "te ver"'
},
{
bisaya: 'Gimingaw ko nimo.',
native: 'Eu sinto sua falta.',
explanation: '"Gimingaw" significa "sentir falta", "ko" é "eu", "nimo" é "você"'
},
{
bisaya: 'Nalipay ko.',
native: 'Estou feliz.',
explanation: '"Nalipay" significa "feliz", "ko" é "eu"'
},
{
bisaya: 'Nasubo ko.',
native: 'Estou triste.',
explanation: '"Nasubo" significa "triste", "ko" é "eu"'
},
{
bisaya: 'Nalipay ko nga naa ka dinhi.',
native: 'Estou feliz que você esteja aqui.',
explanation: '"Nalipay" é "feliz", "nga naa ka dinhi" significa "que você esteja aqui"'
}
]
}
};
async function findOrCreateSystemUser() {
// Versuche zuerst einen System-Benutzer zu finden (z.B. mit username "system" oder "admin")
let systemUser = await User.findOne({
where: {
username: { [sequelize.Sequelize.Op.in]: ['system', 'admin', 'System', 'Admin'] }
}
});
if (!systemUser) {
// Erstelle einen System-Benutzer
const password = crypto.randomBytes(32).toString('hex');
const hashedPassword = await bcrypt.hash(password, 10);
const hashedId = crypto.createHash('sha256').update(`system-${Date.now()}`).digest('hex');
systemUser = await User.create({
username: 'system',
password: hashedPassword,
hashedId: hashedId,
email: 'system@your-part.de'
});
console.log('✅ System-Benutzer erstellt:', systemUser.hashedId);
} else {
console.log('✅ System-Benutzer gefunden:', systemUser.hashedId);
}
return systemUser;
}
function createFeelingsAffectionExercises(nativeLanguageName) {
const exercises = [];
const conversations = FEELINGS_AFFECTION[nativeLanguageName]?.conversations || [];
if (conversations.length === 0) {
console.warn(`⚠️ Keine Gespräche für Muttersprache "${nativeLanguageName}" gefunden. Verwende Deutsch als Fallback.`);
return createFeelingsAffectionExercises('Deutsch');
}
let exerciseNum = 1;
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche)
conversations.forEach((conv, idx) => {
// Erstelle für jedes Gespräch eine Multiple Choice Übung
const wrongOptions = conversations
.filter((c, i) => i !== idx)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map(c => c.native);
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
const correctIndex = options.indexOf(conv.native);
exercises.push({
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNum++,
title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung`,
instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName,
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`,
options: options
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: correctIndex
}),
explanation: conv.explanation
});
});
// Multiple Choice: Rückwärts-Übersetzung (Was bedeutet dieser Satz?)
conversations.forEach((conv, idx) => {
if (idx < 6) { // Erste 6 als Rückwärts-Übersetzung
const wrongOptions = conversations
.filter((c, i) => i !== idx)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map(c => c.native);
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
const correctIndex = options.indexOf(conv.native);
exercises.push({
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNum++,
title: `Gefühle & Zuneigung ${idx + 1} - Was bedeutet dieser Satz?`,
instruction: 'Was bedeutet dieser Bisaya-Satz?',
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Was bedeutet "${conv.bisaya}"?`,
options: options
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: correctIndex
}),
explanation: conv.explanation
});
}
});
// Gap Fill: Vervollständige Gefühlsausdrücke (mehrere Varianten)
exercises.push({
exerciseTypeId: 1, // gap_fill
exerciseNumber: exerciseNum++,
title: 'Gefühle & Zuneigung 1 - Vervollständigen',
instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.',
questionData: JSON.stringify({
type: 'gap_fill',
text: 'Person A: {gap} ko ikaw. (Ich liebe dich)\nPerson B: {gap} ko pud. (Ich liebe dich auch)',
gaps: 2
}),
answerData: JSON.stringify({
type: 'gap_fill',
answers: ['Gihigugma', 'Gihigugma']
}),
explanation: '"Gihigugma" bedeutet "lieben" und wird wiederholt, um "auch" auszudrücken'
});
exercises.push({
exerciseTypeId: 1, // gap_fill
exerciseNumber: exerciseNum++,
title: 'Gefühle & Zuneigung 2 - Vervollständigen',
instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.',
questionData: JSON.stringify({
type: 'gap_fill',
text: 'Person A: {gap} ko nga nakita ka. (Ich bin glücklich)\nPerson B: {gap} ko pud. (Ich auch)',
gaps: 2
}),
answerData: JSON.stringify({
type: 'gap_fill',
answers: ['Nalipay', 'Nalipay']
}),
explanation: '"Nalipay" bedeutet "glücklich sein"'
});
// Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten)
conversations.slice(0, 4).forEach((conv, idx) => {
exercises.push({
exerciseTypeId: 3, // transformation
exerciseNumber: exerciseNum++,
title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung nach Bisaya`,
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: JSON.stringify({
type: 'transformation',
text: conv.native
}),
answerData: JSON.stringify({
type: 'transformation',
correctAnswer: conv.bisaya
}),
explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}`
});
});
return exercises;
}
async function updateFeelingsAffectionExercises() {
await sequelize.authenticate();
console.log('✅ Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
// Finde Bisaya-Sprache mit SQL
const [bisayaLangResult] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!bisayaLangResult) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const bisayaLanguageId = bisayaLangResult.id;
// Hole alle Bisaya-Kurse mit native language info
const courses = await sequelize.query(
`SELECT
c.id,
c.title,
c.native_language_id,
nl.name as native_language_name
FROM community.vocab_course c
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
WHERE c.language_id = :bisayaLanguageId`,
{
replacements: { bisayaLanguageId },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`📚 Gefunden: ${courses.length} Bisaya-Kurse\n`);
let totalExercisesCreated = 0;
let totalLessonsProcessed = 0;
for (const course of courses) {
console.log(`📖 Kurs: ${course.title} (ID: ${course.id})`);
// Finde native language name
const nativeLanguageName = course.native_language_name || 'Deutsch';
console.log(` Muttersprache: ${nativeLanguageName}`);
// Finde "Gefühle & Zuneigung" Lektion
const lessons = await VocabCourseLesson.findAll({
where: {
courseId: course.id,
title: 'Gefühle & Zuneigung'
},
attributes: ['id', 'title', 'lessonNumber']
});
console.log(` ${lessons.length} "Gefühle & Zuneigung"-Lektion(en) gefunden`);
for (const lesson of lessons) {
// Lösche vorhandene Übungen
const deletedCount = await VocabGrammarExercise.destroy({
where: { lessonId: lesson.id }
});
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
// Erstelle neue Übungen
const exercises = createFeelingsAffectionExercises(nativeLanguageName);
if (exercises.length > 0) {
const exercisesToCreate = exercises.map(ex => ({
...ex,
lessonId: lesson.id,
createdByUserId: systemUser.id
}));
await VocabGrammarExercise.bulkCreate(exercisesToCreate);
totalExercisesCreated += exercisesToCreate.length;
console.log(`${exercisesToCreate.length} neue Übung(en) erstellt`);
} else {
console.log(` ⚠️ Keine Übungen erstellt`);
}
totalLessonsProcessed++;
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalLessonsProcessed} "Gefühle & Zuneigung"-Lektion(en) verarbeitet`);
console.log(` ${totalExercisesCreated} Grammatik-Übungen erstellt`);
}
updateFeelingsAffectionExercises()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,730 @@
#!/usr/bin/env node
/**
* Script zum Erstellen von Übungen für die "Essen & Fürsorge" und "Essen & Trinken" Lektionen
*
* Verwendung:
* node backend/scripts/update-food-care-exercises.js
*
* Erstellt Gesprächsübungen für die "Essen & Fürsorge" und "Essen & Trinken" Lektionen in allen Bisaya-Kursen.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import VocabCourse from '../models/community/vocab_course.js';
import User from '../models/community/user.js';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { Op } from 'sequelize';
// Essen & Fürsorge / Essen & Trinken auf Bisaya mit verschiedenen Muttersprachen
const FOOD_CARE_CONVERSATIONS = {
// Deutsch -> Bisaya
'Deutsch': {
'Essen & Fürsorge': [
{
bisaya: 'Gutom na ko.',
native: 'Ich habe Hunger.',
explanation: '"Gutom" bedeutet "Hunger", "na" ist "schon", "ko" ist "ich"'
},
{
bisaya: 'Gihikap ko.',
native: 'Ich habe Durst.',
explanation: '"Gihikap" bedeutet "Durst haben", "ko" ist "ich"'
},
{
bisaya: 'Gusto ka mokaon?',
native: 'Möchtest du essen?',
explanation: '"Gusto" bedeutet "möchten", "ka" ist "du", "mokaon" ist "essen"'
},
{
bisaya: 'Oo, gusto ko.',
native: 'Ja, ich möchte.',
explanation: '"Oo" ist "Ja", "gusto ko" ist "ich möchte"'
},
{
bisaya: 'Unsa ang gusto nimo?',
native: 'Was möchtest du?',
explanation: '"Unsa" ist "Was", "ang" ist Artikel, "gusto nimo" ist "du möchtest"'
},
{
bisaya: 'Gusto ko ug kan-on.',
native: 'Ich möchte Reis.',
explanation: '"Gusto ko" ist "ich möchte", "ug" ist "und/ein", "kan-on" ist "Reis"'
},
{
bisaya: 'Palihug, hatagi ko ug tubig.',
native: 'Bitte gib mir Wasser.',
explanation: '"Palihug" ist "Bitte", "hatagi" ist "geben", "ko" ist "mir", "ug tubig" ist "Wasser"'
},
{
bisaya: 'Salamat sa pagkaon.',
native: 'Danke für das Essen.',
explanation: '"Salamat" ist "Danke", "sa pagkaon" ist "für das Essen"'
},
{
bisaya: 'Lami kaayo!',
native: 'Sehr lecker!',
explanation: '"Lami" bedeutet "lecker", "kaayo" ist "sehr"'
},
{
bisaya: 'Busog na ko.',
native: 'Ich bin satt.',
explanation: '"Busog" bedeutet "satt", "na" ist "schon", "ko" ist "ich"'
},
{
bisaya: 'Kumusta ang pagkaon?',
native: 'Wie schmeckt das Essen?',
explanation: '"Kumusta" ist "Wie", "ang pagkaon" ist "das Essen"'
},
{
bisaya: 'Maayo kaayo ang pagkaon.',
native: 'Das Essen ist sehr gut.',
explanation: '"Maayo" ist "gut", "kaayo" ist "sehr", "ang pagkaon" ist "das Essen"'
}
],
'Essen & Trinken': [
{
bisaya: 'Kan-on',
native: 'Reis',
explanation: '"Kan-on" ist das grundlegende Wort für "Reis"'
},
{
bisaya: 'Tubig',
native: 'Wasser',
explanation: '"Tubig" bedeutet "Wasser"'
},
{
bisaya: 'Pan',
native: 'Brot',
explanation: '"Pan" bedeutet "Brot"'
},
{
bisaya: 'Isda',
native: 'Fisch',
explanation: '"Isda" bedeutet "Fisch"'
},
{
bisaya: 'Manok',
native: 'Huhn',
explanation: '"Manok" bedeutet "Huhn"'
},
{
bisaya: 'Baboy',
native: 'Schwein',
explanation: '"Baboy" bedeutet "Schwein"'
},
{
bisaya: 'Gulay',
native: 'Gemüse',
explanation: '"Gulay" bedeutet "Gemüse"'
},
{
bisaya: 'Prutas',
native: 'Obst',
explanation: '"Prutas" bedeutet "Obst"'
},
{
bisaya: 'Gatas',
native: 'Milch',
explanation: '"Gatas" bedeutet "Milch"'
},
{
bisaya: 'Kape',
native: 'Kaffee',
explanation: '"Kape" bedeutet "Kaffee"'
},
{
bisaya: 'Tsa',
native: 'Tee',
explanation: '"Tsa" bedeutet "Tee"'
},
{
bisaya: 'Asin',
native: 'Salz',
explanation: '"Asin" bedeutet "Salz"'
},
{
bisaya: 'Asukar',
native: 'Zucker',
explanation: '"Asukar" bedeutet "Zucker"'
},
{
bisaya: 'Tinapay',
native: 'Brot (alternativ)',
explanation: '"Tinapay" ist eine alternative Bezeichnung für "Brot"'
},
{
bisaya: 'Bugas',
native: 'Reis (roh)',
explanation: '"Bugas" ist ungekochter Reis, "kan-on" ist gekochter Reis'
}
]
},
// Englisch -> Bisaya
'Englisch': {
'Essen & Fürsorge': [
{
bisaya: 'Gutom na ko.',
native: 'I am hungry.',
explanation: '"Gutom" means "hungry", "na" is "already", "ko" is "I"'
},
{
bisaya: 'Gihikap ko.',
native: 'I am thirsty.',
explanation: '"Gihikap" means "thirsty", "ko" is "I"'
},
{
bisaya: 'Gusto ka mokaon?',
native: 'Do you want to eat?',
explanation: '"Gusto" means "want", "ka" is "you", "mokaon" is "to eat"'
},
{
bisaya: 'Oo, gusto ko.',
native: 'Yes, I want.',
explanation: '"Oo" is "Yes", "gusto ko" is "I want"'
},
{
bisaya: 'Unsa ang gusto nimo?',
native: 'What do you want?',
explanation: '"Unsa" is "What", "ang" is article, "gusto nimo" is "you want"'
},
{
bisaya: 'Gusto ko ug kan-on.',
native: 'I want rice.',
explanation: '"Gusto ko" is "I want", "ug" is "a/some", "kan-on" is "rice"'
},
{
bisaya: 'Palihug, hatagi ko ug tubig.',
native: 'Please give me water.',
explanation: '"Palihug" is "Please", "hatagi" is "give", "ko" is "me", "ug tubig" is "water"'
},
{
bisaya: 'Salamat sa pagkaon.',
native: 'Thank you for the food.',
explanation: '"Salamat" is "Thank you", "sa pagkaon" is "for the food"'
},
{
bisaya: 'Lami kaayo!',
native: 'Very delicious!',
explanation: '"Lami" means "delicious", "kaayo" is "very"'
},
{
bisaya: 'Busog na ko.',
native: 'I am full.',
explanation: '"Busog" means "full", "na" is "already", "ko" is "I"'
},
{
bisaya: 'Kumusta ang pagkaon?',
native: 'How is the food?',
explanation: '"Kumusta" is "How", "ang pagkaon" is "the food"'
},
{
bisaya: 'Maayo kaayo ang pagkaon.',
native: 'The food is very good.',
explanation: '"Maayo" is "good", "kaayo" is "very", "ang pagkaon" is "the food"'
}
],
'Essen & Trinken': [
{
bisaya: 'Kan-on',
native: 'Rice',
explanation: '"Kan-on" is the basic word for "rice"'
},
{
bisaya: 'Tubig',
native: 'Water',
explanation: '"Tubig" means "water"'
},
{
bisaya: 'Pan',
native: 'Bread',
explanation: '"Pan" means "bread"'
},
{
bisaya: 'Isda',
native: 'Fish',
explanation: '"Isda" means "fish"'
},
{
bisaya: 'Manok',
native: 'Chicken',
explanation: '"Manok" means "chicken"'
},
{
bisaya: 'Baboy',
native: 'Pig',
explanation: '"Baboy" means "pig"'
},
{
bisaya: 'Gulay',
native: 'Vegetables',
explanation: '"Gulay" means "vegetables"'
},
{
bisaya: 'Prutas',
native: 'Fruit',
explanation: '"Prutas" means "fruit"'
},
{
bisaya: 'Gatas',
native: 'Milk',
explanation: '"Gatas" means "milk"'
},
{
bisaya: 'Kape',
native: 'Coffee',
explanation: '"Kape" means "coffee"'
},
{
bisaya: 'Tsa',
native: 'Tea',
explanation: '"Tsa" means "tea"'
},
{
bisaya: 'Asin',
native: 'Salt',
explanation: '"Asin" means "salt"'
},
{
bisaya: 'Asukar',
native: 'Sugar',
explanation: '"Asukar" means "sugar"'
},
{
bisaya: 'Tinapay',
native: 'Bread (alternative)',
explanation: '"Tinapay" is an alternative term for "bread"'
},
{
bisaya: 'Bugas',
native: 'Rice (uncooked)',
explanation: '"Bugas" is uncooked rice, "kan-on" is cooked rice'
}
]
}
};
// Erweitere für weitere Sprachen (Spanisch, Französisch, Italienisch, Portugiesisch, Tagalog)
const ADDITIONAL_LANGUAGES = {
'Spanisch': {
'Essen & Fürsorge': [
{ bisaya: 'Gutom na ko.', native: 'Tengo hambre.', explanation: '"Gutom" significa "hambre", "na" es "ya", "ko" es "yo"' },
{ bisaya: 'Gihikap ko.', native: 'Tengo sed.', explanation: '"Gihikap" significa "sed", "ko" es "yo"' },
{ bisaya: 'Gusto ka mokaon?', native: '¿Quieres comer?', explanation: '"Gusto" significa "querer", "ka" es "tú", "mokaon" es "comer"' },
{ bisaya: 'Oo, gusto ko.', native: 'Sí, quiero.', explanation: '"Oo" es "Sí", "gusto ko" es "quiero"' },
{ bisaya: 'Unsa ang gusto nimo?', native: '¿Qué quieres?', explanation: '"Unsa" es "Qué", "ang" es artículo, "gusto nimo" es "quieres"' },
{ bisaya: 'Gusto ko ug kan-on.', native: 'Quiero arroz.', explanation: '"Gusto ko" es "quiero", "ug kan-on" es "arroz"' },
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Por favor, dame agua.', explanation: '"Palihug" es "Por favor", "hatagi" es "dar", "ko" es "me", "ug tubig" es "agua"' },
{ bisaya: 'Salamat sa pagkaon.', native: 'Gracias por la comida.', explanation: '"Salamat" es "Gracias", "sa pagkaon" es "por la comida"' },
{ bisaya: 'Lami kaayo!', native: '¡Muy delicioso!', explanation: '"Lami" significa "delicioso", "kaayo" es "muy"' },
{ bisaya: 'Busog na ko.', native: 'Estoy lleno.', explanation: '"Busog" significa "lleno", "na" es "ya", "ko" es "yo"' },
{ bisaya: 'Kumusta ang pagkaon?', native: '¿Cómo está la comida?', explanation: '"Kumusta" es "Cómo", "ang pagkaon" es "la comida"' },
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'La comida está muy buena.', explanation: '"Maayo" es "buena", "kaayo" es "muy", "ang pagkaon" es "la comida"' }
],
'Essen & Trinken': [
{ bisaya: 'Kan-on', native: 'Arroz', explanation: '"Kan-on" es la palabra básica para "arroz"' },
{ bisaya: 'Tubig', native: 'Agua', explanation: '"Tubig" significa "agua"' },
{ bisaya: 'Pan', native: 'Pan', explanation: '"Pan" significa "pan"' },
{ bisaya: 'Isda', native: 'Pescado', explanation: '"Isda" significa "pescado"' },
{ bisaya: 'Manok', native: 'Pollo', explanation: '"Manok" significa "pollo"' },
{ bisaya: 'Baboy', native: 'Cerdo', explanation: '"Baboy" significa "cerdo"' },
{ bisaya: 'Gulay', native: 'Verduras', explanation: '"Gulay" significa "verduras"' },
{ bisaya: 'Prutas', native: 'Fruta', explanation: '"Prutas" significa "fruta"' },
{ bisaya: 'Gatas', native: 'Leche', explanation: '"Gatas" significa "leche"' },
{ bisaya: 'Kape', native: 'Café', explanation: '"Kape" significa "café"' },
{ bisaya: 'Tsa', native: 'Té', explanation: '"Tsa" significa "té"' },
{ bisaya: 'Asin', native: 'Sal', explanation: '"Asin" significa "sal"' },
{ bisaya: 'Asukar', native: 'Azúcar', explanation: '"Asukar" significa "azúcar"' },
{ bisaya: 'Tinapay', native: 'Pan (alternativo)', explanation: '"Tinapay" es un término alternativo para "pan"' },
{ bisaya: 'Bugas', native: 'Arroz (crudo)', explanation: '"Bugas" es arroz crudo, "kan-on" es arroz cocido' }
]
},
'Französisch': {
'Essen & Fürsorge': [
{ bisaya: 'Gutom na ko.', native: 'J\'ai faim.', explanation: '"Gutom" signifie "faim", "na" est "déjà", "ko" est "je"' },
{ bisaya: 'Gihikap ko.', native: 'J\'ai soif.', explanation: '"Gihikap" signifie "soif", "ko" est "je"' },
{ bisaya: 'Gusto ka mokaon?', native: 'Tu veux manger?', explanation: '"Gusto" signifie "vouloir", "ka" est "tu", "mokaon" est "manger"' },
{ bisaya: 'Oo, gusto ko.', native: 'Oui, je veux.', explanation: '"Oo" est "Oui", "gusto ko" est "je veux"' },
{ bisaya: 'Unsa ang gusto nimo?', native: 'Que veux-tu?', explanation: '"Unsa" est "Que", "ang" est article, "gusto nimo" est "tu veux"' },
{ bisaya: 'Gusto ko ug kan-on.', native: 'Je veux du riz.', explanation: '"Gusto ko" est "je veux", "ug kan-on" est "riz"' },
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'S\'il te plaît, donne-moi de l\'eau.', explanation: '"Palihug" est "S\'il te plaît", "hatagi" est "donner", "ko" est "moi", "ug tubig" est "eau"' },
{ bisaya: 'Salamat sa pagkaon.', native: 'Merci pour la nourriture.', explanation: '"Salamat" est "Merci", "sa pagkaon" est "pour la nourriture"' },
{ bisaya: 'Lami kaayo!', native: 'Très délicieux!', explanation: '"Lami" signifie "délicieux", "kaayo" est "très"' },
{ bisaya: 'Busog na ko.', native: 'Je suis rassasié.', explanation: '"Busog" signifie "rassasié", "na" est "déjà", "ko" est "je"' },
{ bisaya: 'Kumusta ang pagkaon?', native: 'Comment est la nourriture?', explanation: '"Kumusta" est "Comment", "ang pagkaon" est "la nourriture"' },
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'La nourriture est très bonne.', explanation: '"Maayo" est "bonne", "kaayo" est "très", "ang pagkaon" est "la nourriture"' }
],
'Essen & Trinken': [
{ bisaya: 'Kan-on', native: 'Riz', explanation: '"Kan-on" est le mot de base pour "riz"' },
{ bisaya: 'Tubig', native: 'Eau', explanation: '"Tubig" signifie "eau"' },
{ bisaya: 'Pan', native: 'Pain', explanation: '"Pan" signifie "pain"' },
{ bisaya: 'Isda', native: 'Poisson', explanation: '"Isda" signifie "poisson"' },
{ bisaya: 'Manok', native: 'Poulet', explanation: '"Manok" signifie "poulet"' },
{ bisaya: 'Baboy', native: 'Porc', explanation: '"Baboy" signifie "porc"' },
{ bisaya: 'Gulay', native: 'Légumes', explanation: '"Gulay" signifie "légumes"' },
{ bisaya: 'Prutas', native: 'Fruit', explanation: '"Prutas" signifie "fruit"' },
{ bisaya: 'Gatas', native: 'Lait', explanation: '"Gatas" signifie "lait"' },
{ bisaya: 'Kape', native: 'Café', explanation: '"Kape" signifie "café"' },
{ bisaya: 'Tsa', native: 'Thé', explanation: '"Tsa" signifie "thé"' },
{ bisaya: 'Asin', native: 'Sel', explanation: '"Asin" signifie "sel"' },
{ bisaya: 'Asukar', native: 'Sucre', explanation: '"Asukar" signifie "sucre"' },
{ bisaya: 'Tinapay', native: 'Pain (alternatif)', explanation: '"Tinapay" est un terme alternatif pour "pain"' },
{ bisaya: 'Bugas', native: 'Riz (cru)', explanation: '"Bugas" est riz cru, "kan-on" est riz cuit' }
]
},
'Italienisch': {
'Essen & Fürsorge': [
{ bisaya: 'Gutom na ko.', native: 'Ho fame.', explanation: '"Gutom" significa "fame", "na" è "già", "ko" è "io"' },
{ bisaya: 'Gihikap ko.', native: 'Ho sete.', explanation: '"Gihikap" significa "sete", "ko" è "io"' },
{ bisaya: 'Gusto ka mokaon?', native: 'Vuoi mangiare?', explanation: '"Gusto" significa "volere", "ka" è "tu", "mokaon" è "mangiare"' },
{ bisaya: 'Oo, gusto ko.', native: 'Sì, voglio.', explanation: '"Oo" è "Sì", "gusto ko" è "voglio"' },
{ bisaya: 'Unsa ang gusto nimo?', native: 'Cosa vuoi?', explanation: '"Unsa" è "Cosa", "ang" è articolo, "gusto nimo" è "vuoi"' },
{ bisaya: 'Gusto ko ug kan-on.', native: 'Voglio riso.', explanation: '"Gusto ko" è "voglio", "ug kan-on" è "riso"' },
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Per favore, dammi acqua.', explanation: '"Palihug" è "Per favore", "hatagi" è "dare", "ko" è "mi", "ug tubig" è "acqua"' },
{ bisaya: 'Salamat sa pagkaon.', native: 'Grazie per il cibo.', explanation: '"Salamat" è "Grazie", "sa pagkaon" è "per il cibo"' },
{ bisaya: 'Lami kaayo!', native: 'Molto delizioso!', explanation: '"Lami" significa "delizioso", "kaayo" è "molto"' },
{ bisaya: 'Busog na ko.', native: 'Sono sazio.', explanation: '"Busog" significa "sazio", "na" è "già", "ko" è "io"' },
{ bisaya: 'Kumusta ang pagkaon?', native: 'Com\'è il cibo?', explanation: '"Kumusta" è "Come", "ang pagkaon" è "il cibo"' },
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'Il cibo è molto buono.', explanation: '"Maayo" è "buono", "kaayo" è "molto", "ang pagkaon" è "il cibo"' }
],
'Essen & Trinken': [
{ bisaya: 'Kan-on', native: 'Riso', explanation: '"Kan-on" è la parola base per "riso"' },
{ bisaya: 'Tubig', native: 'Acqua', explanation: '"Tubig" significa "acqua"' },
{ bisaya: 'Pan', native: 'Pane', explanation: '"Pan" significa "pane"' },
{ bisaya: 'Isda', native: 'Pesce', explanation: '"Isda" significa "pesce"' },
{ bisaya: 'Manok', native: 'Pollo', explanation: '"Manok" significa "pollo"' },
{ bisaya: 'Baboy', native: 'Maiale', explanation: '"Baboy" significa "maiale"' },
{ bisaya: 'Gulay', native: 'Verdura', explanation: '"Gulay" significa "verdura"' },
{ bisaya: 'Prutas', native: 'Frutta', explanation: '"Prutas" significa "frutta"' },
{ bisaya: 'Gatas', native: 'Latte', explanation: '"Gatas" significa "latte"' },
{ bisaya: 'Kape', native: 'Caffè', explanation: '"Kape" significa "caffè"' },
{ bisaya: 'Tsa', native: 'Tè', explanation: '"Tsa" significa "tè"' },
{ bisaya: 'Asin', native: 'Sale', explanation: '"Asin" significa "sale"' },
{ bisaya: 'Asukar', native: 'Zucchero', explanation: '"Asukar" significa "zucchero"' },
{ bisaya: 'Tinapay', native: 'Pane (alternativo)', explanation: '"Tinapay" è un termine alternativo per "pane"' },
{ bisaya: 'Bugas', native: 'Riso (crudo)', explanation: '"Bugas" è riso crudo, "kan-on" è riso cotto' }
]
},
'Portugiesisch': {
'Essen & Fürsorge': [
{ bisaya: 'Gutom na ko.', native: 'Tenho fome.', explanation: '"Gutom" significa "fome", "na" é "já", "ko" é "eu"' },
{ bisaya: 'Gihikap ko.', native: 'Tenho sede.', explanation: '"Gihikap" significa "sede", "ko" é "eu"' },
{ bisaya: 'Gusto ka mokaon?', native: 'Quer comer?', explanation: '"Gusto" significa "querer", "ka" é "você", "mokaon" é "comer"' },
{ bisaya: 'Oo, gusto ko.', native: 'Sim, quero.', explanation: '"Oo" é "Sim", "gusto ko" é "quero"' },
{ bisaya: 'Unsa ang gusto nimo?', native: 'O que você quer?', explanation: '"Unsa" é "O que", "ang" é artigo, "gusto nimo" é "você quer"' },
{ bisaya: 'Gusto ko ug kan-on.', native: 'Quero arroz.', explanation: '"Gusto ko" é "quero", "ug kan-on" é "arroz"' },
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Por favor, me dê água.', explanation: '"Palihug" é "Por favor", "hatagi" é "dar", "ko" é "me", "ug tubig" é "água"' },
{ bisaya: 'Salamat sa pagkaon.', native: 'Obrigado pela comida.', explanation: '"Salamat" é "Obrigado", "sa pagkaon" é "pela comida"' },
{ bisaya: 'Lami kaayo!', native: 'Muito delicioso!', explanation: '"Lami" significa "delicioso", "kaayo" é "muito"' },
{ bisaya: 'Busog na ko.', native: 'Estou cheio.', explanation: '"Busog" significa "cheio", "na" é "já", "ko" é "eu"' },
{ bisaya: 'Kumusta ang pagkaon?', native: 'Como está a comida?', explanation: '"Kumusta" é "Como", "ang pagkaon" é "a comida"' },
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'A comida está muito boa.', explanation: '"Maayo" é "boa", "kaayo" é "muito", "ang pagkaon" é "a comida"' }
],
'Essen & Trinken': [
{ bisaya: 'Kan-on', native: 'Arroz', explanation: '"Kan-on" é a palavra básica para "arroz"' },
{ bisaya: 'Tubig', native: 'Água', explanation: '"Tubig" significa "água"' },
{ bisaya: 'Pan', native: 'Pão', explanation: '"Pan" significa "pão"' },
{ bisaya: 'Isda', native: 'Peixe', explanation: '"Isda" significa "peixe"' },
{ bisaya: 'Manok', native: 'Frango', explanation: '"Manok" significa "frango"' },
{ bisaya: 'Baboy', native: 'Porco', explanation: '"Baboy" significa "porco"' },
{ bisaya: 'Gulay', native: 'Legumes', explanation: '"Gulay" significa "legumes"' },
{ bisaya: 'Prutas', native: 'Fruta', explanation: '"Prutas" significa "fruta"' },
{ bisaya: 'Gatas', native: 'Leite', explanation: '"Gatas" significa "leite"' },
{ bisaya: 'Kape', native: 'Café', explanation: '"Kape" significa "café"' },
{ bisaya: 'Tsa', native: 'Chá', explanation: '"Tsa" significa "chá"' },
{ bisaya: 'Asin', native: 'Sal', explanation: '"Asin" significa "sal"' },
{ bisaya: 'Asukar', native: 'Açúcar', explanation: '"Asukar" significa "açúcar"' },
{ bisaya: 'Tinapay', native: 'Pão (alternativo)', explanation: '"Tinapay" é um termo alternativo para "pão"' },
{ bisaya: 'Bugas', native: 'Arroz (cru)', explanation: '"Bugas" é arroz cru, "kan-on" é arroz cozido' }
]
},
'Tagalog': {
'Essen & Fürsorge': [
{ bisaya: 'Gutom na ko.', native: 'Gutom na ako.', explanation: '"Gutom" ay "gutom", "na" ay "na", "ko" ay "ako"' },
{ bisaya: 'Gihikap ko.', native: 'Nauuhaw ako.', explanation: '"Gihikap" ay "nauuhaw", "ko" ay "ako"' },
{ bisaya: 'Gusto ka mokaon?', native: 'Gusto mo bang kumain?', explanation: '"Gusto" ay "gusto", "ka" ay "mo", "mokaon" ay "kumain"' },
{ bisaya: 'Oo, gusto ko.', native: 'Oo, gusto ko.', explanation: '"Oo" ay "Oo", "gusto ko" ay "gusto ko"' },
{ bisaya: 'Unsa ang gusto nimo?', native: 'Ano ang gusto mo?', explanation: '"Unsa" ay "Ano", "ang" ay "ang", "gusto nimo" ay "gusto mo"' },
{ bisaya: 'Gusto ko ug kan-on.', native: 'Gusto ko ng kanin.', explanation: '"Gusto ko" ay "gusto ko", "ug kan-on" ay "kanin"' },
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Pakiusap, bigyan mo ako ng tubig.', explanation: '"Palihug" ay "Pakiusap", "hatagi" ay "bigyan", "ko" ay "ako", "ug tubig" ay "tubig"' },
{ bisaya: 'Salamat sa pagkaon.', native: 'Salamat sa pagkain.', explanation: '"Salamat" ay "Salamat", "sa pagkaon" ay "sa pagkain"' },
{ bisaya: 'Lami kaayo!', native: 'Masarap talaga!', explanation: '"Lami" ay "masarap", "kaayo" ay "talaga"' },
{ bisaya: 'Busog na ko.', native: 'Busog na ako.', explanation: '"Busog" ay "busog", "na" ay "na", "ko" ay "ako"' },
{ bisaya: 'Kumusta ang pagkaon?', native: 'Kumusta ang pagkain?', explanation: '"Kumusta" ay "Kumusta", "ang pagkaon" ay "ang pagkain"' },
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'Mabuti talaga ang pagkain.', explanation: '"Maayo" ay "mabuti", "kaayo" ay "talaga", "ang pagkaon" ay "ang pagkain"' }
],
'Essen & Trinken': [
{ bisaya: 'Kan-on', native: 'Kanin', explanation: '"Kan-on" ay ang salitang base para sa "kanin"' },
{ bisaya: 'Tubig', native: 'Tubig', explanation: '"Tubig" ay "tubig"' },
{ bisaya: 'Pan', native: 'Tinapay', explanation: '"Pan" ay "tinapay"' },
{ bisaya: 'Isda', native: 'Isda', explanation: '"Isda" ay "isda"' },
{ bisaya: 'Manok', native: 'Manok', explanation: '"Manok" ay "manok"' },
{ bisaya: 'Baboy', native: 'Baboy', explanation: '"Baboy" ay "baboy"' },
{ bisaya: 'Gulay', native: 'Gulay', explanation: '"Gulay" ay "gulay"' },
{ bisaya: 'Prutas', native: 'Prutas', explanation: '"Prutas" ay "prutas"' },
{ bisaya: 'Gatas', native: 'Gatas', explanation: '"Gatas" ay "gatas"' },
{ bisaya: 'Kape', native: 'Kape', explanation: '"Kape" ay "kape"' },
{ bisaya: 'Tsa', native: 'Tsa', explanation: '"Tsa" ay "tsa"' },
{ bisaya: 'Asin', native: 'Asin', explanation: '"Asin" ay "asin"' },
{ bisaya: 'Asukar', native: 'Asukal', explanation: '"Asukar" ay "asukal"' },
{ bisaya: 'Tinapay', native: 'Tinapay', explanation: '"Tinapay" ay "tinapay"' },
{ bisaya: 'Bugas', native: 'Bigas', explanation: '"Bugas" ay "bigas", "kan-on" ay "kanin"' }
]
}
};
// Kombiniere alle Sprachen
const ALL_FOOD_CARE = { ...FOOD_CARE_CONVERSATIONS, ...ADDITIONAL_LANGUAGES };
async function findOrCreateSystemUser() {
let systemUser = await User.findOne({
where: {
username: { [Op.in]: ['system', 'admin', 'System', 'Admin'] }
}
});
if (!systemUser) {
console.error('❌ System-Benutzer nicht gefunden.');
throw new Error('System user not found');
}
return systemUser;
}
async function updateFoodCareExercises() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
// Finde alle Bisaya-Kurse
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const bisayaLanguageId = bisayaLanguage.id;
// Hole alle Bisaya-Kurse mit native language info
const courses = await sequelize.query(
`SELECT
c.id,
c.title,
c.owner_user_id,
c.native_language_id,
nl.name as native_language_name
FROM community.vocab_course c
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
WHERE c.language_id = :bisayaLanguageId`,
{
replacements: { bisayaLanguageId },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
let totalExercisesCreated = 0;
let totalLessonsUpdated = 0;
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
const nativeLangName = course.native_language_name || 'Deutsch';
console.log(` Muttersprache: ${nativeLangName}`);
// Finde "Essen & Fürsorge" und "Essen & Trinken" Lektionen
const lessons = await VocabCourseLesson.findAll({
where: {
courseId: course.id,
title: ['Essen & Fürsorge', 'Essen & Trinken']
},
order: [['lessonNumber', 'ASC']]
});
console.log(` ${lessons.length} Lektion(en) gefunden\n`);
for (const lesson of lessons) {
const conversations = ALL_FOOD_CARE[nativeLangName]?.[lesson.title];
if (!conversations || conversations.length === 0) {
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen für Muttersprache "${nativeLangName}" definiert`);
continue;
}
// Lösche bestehende Übungen
const deletedCount = await VocabGrammarExercise.destroy({
where: { lessonId: lesson.id }
});
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
// Erstelle neue Übungen basierend auf dem Lektionstyp
let exerciseNumber = 1;
if (lesson.title === 'Essen & Fürsorge') {
// Gesprächsübungen für "Essen & Fürsorge"
for (const conv of conversations) {
// Multiple Choice: Übersetzung von Muttersprache zu Bisaya
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNumber++,
title: `Wie sagt man "${conv.native}"?`,
instruction: 'Wähle die richtige Übersetzung.',
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Wie sagt man "${conv.native}" auf Bisaya?`,
options: [
conv.bisaya,
conversations[(exerciseNumber - 2 + 1) % conversations.length]?.bisaya || 'Salamat',
conversations[(exerciseNumber - 2 + 2) % conversations.length]?.bisaya || 'Maayo',
conversations[(exerciseNumber - 2 + 3) % conversations.length]?.bisaya || 'Palihug'
]
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: 0
}),
explanation: conv.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesCreated++;
// Multiple Choice: Übersetzung von Bisaya zu Muttersprache
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNumber++,
title: `Was bedeutet "${conv.bisaya}"?`,
instruction: 'Wähle die richtige Übersetzung.',
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Was bedeutet "${conv.bisaya}"?`,
options: [
conv.native,
conversations[(exerciseNumber - 3 + 1) % conversations.length]?.native || 'Danke',
conversations[(exerciseNumber - 3 + 2) % conversations.length]?.native || 'Bitte',
conversations[(exerciseNumber - 3 + 3) % conversations.length]?.native || 'Gut'
]
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: 0
}),
explanation: conv.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesCreated++;
}
// Transformation-Übungen
const selectedConvs = conversations.slice(0, 3);
for (const conv of selectedConvs) {
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: 4, // transformation
exerciseNumber: exerciseNumber++,
title: `Übersetze: "${conv.native}"`,
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: JSON.stringify({
type: 'transformation',
text: conv.native,
sourceLanguage: nativeLangName,
targetLanguage: 'Bisaya'
}),
answerData: JSON.stringify({
type: 'transformation',
correct: conv.bisaya,
alternatives: []
}),
explanation: conv.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesCreated++;
}
} else if (lesson.title === 'Essen & Trinken') {
// Vokabular-Übungen für "Essen & Trinken"
for (const vocab of conversations) {
// Multiple Choice: Muttersprache -> Bisaya
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNumber++,
title: `Wie sagt man "${vocab.native}"?`,
instruction: 'Wähle die richtige Übersetzung.',
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Wie sagt man "${vocab.native}" auf Bisaya?`,
options: [
vocab.bisaya,
conversations[(exerciseNumber - 2 + 1) % conversations.length]?.bisaya || 'Salamat',
conversations[(exerciseNumber - 2 + 2) % conversations.length]?.bisaya || 'Maayo',
conversations[(exerciseNumber - 2 + 3) % conversations.length]?.bisaya || 'Palihug'
]
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: 0
}),
explanation: vocab.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesCreated++;
// Multiple Choice: Bisaya -> Muttersprache
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: 2, // multiple_choice
exerciseNumber: exerciseNumber++,
title: `Was bedeutet "${vocab.bisaya}"?`,
instruction: 'Wähle die richtige Übersetzung.',
questionData: JSON.stringify({
type: 'multiple_choice',
question: `Was bedeutet "${vocab.bisaya}"?`,
options: [
vocab.native,
conversations[(exerciseNumber - 3 + 1) % conversations.length]?.native || 'Danke',
conversations[(exerciseNumber - 3 + 2) % conversations.length]?.native || 'Bitte',
conversations[(exerciseNumber - 3 + 3) % conversations.length]?.native || 'Gut'
]
}),
answerData: JSON.stringify({
type: 'multiple_choice',
correctAnswer: 0
}),
explanation: vocab.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesCreated++;
}
}
}
console.log(`${lessons.length} Lektion(en) aktualisiert\n`);
totalLessonsUpdated += lessons.length;
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalLessonsUpdated} Lektionen aktualisiert`);
console.log(` ${totalExercisesCreated} neue Grammatik-Übungen erstellt`);
}
updateFoodCareExercises()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,454 @@
#!/usr/bin/env node
/**
* Script zum Aktualisieren der "Überlebenssätze"-Übungen in Bisaya-Kursen
*
* Verwendung:
* node backend/scripts/update-survival-sentences-exercises.js
*
* Ersetzt bestehende generische Übungen durch spezifische Überlebenssätze-Übungen.
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import User from '../models/community/user.js';
// Spezifische Übungen für Überlebenssätze
const SURVIVAL_EXERCISES = {
'Überlebenssätze - Teil 1': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Ich verstehe nicht"?',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
options: ['Wala ko kasabot', 'Palihug', 'Salamat', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht" - sehr wichtig für Anfänger!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Kannst du das wiederholen?"?',
instruction: 'Wähle die richtige Bitte aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Kannst du das wiederholen?" auf Bisaya?',
options: ['Palihug ka mubalik?', 'Salamat', 'Maayo', 'Kumusta ka?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?" - essentiell für das Lernen!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Wo ist...?"?',
instruction: 'Wähle die richtige Frage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wo ist die Toilette?" auf Bisaya?',
options: ['Asa ang CR?', 'Kumusta ka?', 'Salamat', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Asa ang CR?" bedeutet "Wo ist die Toilette?" - "Asa" = "Wo", "CR" = "Comfort Room" (Toilette).'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Vervollständige den Satz: "Ich verstehe nicht"',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
options: ['Wala ko kasabot', 'Dili ko kasabot', 'Wala ko makasabot', 'Dili ko makasabot']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht".'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Vervollständige den Satz: "Bitte wiederholen"',
instruction: 'Wähle die richtige Übersetzung.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Bitte wiederholen" auf Bisaya?',
options: ['Palihug ka mubalik?', 'Palihug balik', 'Salamat mubalik', 'Maayo mubalik']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?".'
}
],
'Überlebenssätze - Teil 2': [
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Wie viel kostet das?"?',
instruction: 'Wähle die richtige Frage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Wie viel kostet das?" auf Bisaya?',
options: ['Tagpila ni?', 'Asa ni?', 'Unsa ni?', 'Kinsa ni?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Entschuldigung"?',
instruction: 'Wähle die richtige Entschuldigung aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Entschuldigung" auf Bisaya?',
options: ['Pasensya', 'Salamat', 'Palihug', 'Maayo']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Was ist das?"?',
instruction: 'Wähle die richtige Frage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Was ist das?" auf Bisaya?',
options: ['Unsa ni?', 'Asa ni?', 'Tagpila ni?', 'Kinsa ni?']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Unsa ni?" bedeutet "Was ist das?" - "Unsa" = "Was", "ni" = "das".'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Bitte langsam"?',
instruction: 'Wähle die richtige Bitte aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Bitte langsam" auf Bisaya?',
options: ['Hinay-hinay lang', 'Palihug lang', 'Maayo lang', 'Salamat lang']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Hinay-hinay lang" bedeutet "Bitte langsam" - sehr wichtig, wenn jemand zu schnell spricht!'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Ich spreche kein Bisaya"?',
instruction: 'Wähle die richtige Aussage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Ich spreche kein Bisaya" auf Bisaya?',
options: ['Dili ko mag-Bisaya', 'Wala ko mag-Bisaya', 'Maayo ko mag-Bisaya', 'Salamat ko mag-Bisaya']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Kannst du das aufschreiben?"?',
instruction: 'Wähle die richtige Bitte aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Kannst du das aufschreiben?" auf Bisaya?',
options: ['Palihug isulat ni', 'Palihug basahon ni', 'Palihug sulaton ni', 'Palihug pakigamit ni']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Palihug isulat ni" bedeutet "Bitte schreibe das auf" - hilfreich beim Lernen neuer Wörter.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Wie sagt man "Ich bin verloren"?',
instruction: 'Wähle die richtige Aussage aus.',
questionData: {
type: 'multiple_choice',
question: 'Wie sagt man "Ich bin verloren" auf Bisaya?',
options: ['Nawala ko', 'Naa ko', 'Maayo ko', 'Salamat ko']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Nawala ko" bedeutet "Ich bin verloren" - wichtig, wenn du Hilfe brauchst.'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Wichtige Fragen bilden',
instruction: 'Fülle die Lücken mit den richtigen Fragewörtern.',
questionData: {
type: 'gap_fill',
text: '{gap} ni? (Wie viel kostet das?) | {gap} ni? (Was ist das?) | {gap} lang (Bitte langsam)',
gaps: 3
},
answerData: {
type: 'gap_fill',
answers: ['Tagpila', 'Unsa', 'Hinay-hinay']
},
explanation: '"Tagpila" = "Wie viel", "Unsa" = "Was", "Hinay-hinay lang" = "Bitte langsam".'
},
{
exerciseTypeId: 1, // gap_fill
title: 'Überlebenssätze vervollständigen',
instruction: 'Fülle die Lücken mit den richtigen Wörtern.',
questionData: {
type: 'gap_fill',
text: 'Palihug {gap} ni (Bitte schreibe das auf) | {gap} ko (Ich bin verloren) | Dili ko {gap} (Ich spreche kein Bisaya)',
gaps: 3
},
answerData: {
type: 'gap_fill',
answers: ['isulat', 'Nawala', 'mag-Bisaya']
},
explanation: '"isulat" = "aufschreiben", "Nawala" = "verloren", "mag-Bisaya" = "Bisaya sprechen".'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Was bedeutet "Tagpila"?',
instruction: 'Wähle die richtige Bedeutung aus.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Tagpila"?',
options: ['Wie viel', 'Was', 'Wo', 'Wer']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Tagpila" bedeutet "Wie viel" und wird verwendet, um nach Preisen zu fragen.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Was bedeutet "Pasensya"?',
instruction: 'Wähle die richtige Bedeutung aus.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Pasensya"?',
options: ['Entschuldigung', 'Danke', 'Bitte', 'Gut']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
},
{
exerciseTypeId: 2, // multiple_choice
title: 'Was bedeutet "Hinay-hinay lang"?',
instruction: 'Wähle die richtige Bedeutung aus.',
questionData: {
type: 'multiple_choice',
question: 'Was bedeutet "Hinay-hinay lang"?',
options: ['Bitte langsam', 'Bitte schnell', 'Bitte laut', 'Bitte leise']
},
answerData: {
type: 'multiple_choice',
correctAnswer: 0
},
explanation: '"Hinay-hinay lang" bedeutet "Bitte langsam" - sehr wichtig, wenn jemand zu schnell spricht!'
},
{
exerciseTypeId: 4, // transformation
title: 'Überlebenssätze übersetzen - Einkaufen',
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: {
type: 'transformation',
text: 'Wie viel kostet das?',
sourceLanguage: 'Deutsch',
targetLanguage: 'Bisaya'
},
answerData: {
type: 'transformation',
correct: 'Tagpila ni?',
alternatives: ['Tagpila kini?', 'Pila ni?']
},
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
},
{
exerciseTypeId: 4, // transformation
title: 'Überlebenssätze übersetzen - Kommunikation',
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: {
type: 'transformation',
text: 'Ich spreche kein Bisaya',
sourceLanguage: 'Deutsch',
targetLanguage: 'Bisaya'
},
answerData: {
type: 'transformation',
correct: 'Dili ko mag-Bisaya',
alternatives: ['Wala ko mag-Bisaya', 'Dili ko makasabot Bisaya']
},
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
},
{
exerciseTypeId: 4, // transformation
title: 'Überlebenssätze übersetzen - Hilfe',
instruction: 'Übersetze den Satz ins Bisaya.',
questionData: {
type: 'transformation',
text: 'Ich bin verloren',
sourceLanguage: 'Deutsch',
targetLanguage: 'Bisaya'
},
answerData: {
type: 'transformation',
correct: 'Nawala ko',
alternatives: ['Nawala ako', 'Nawala na ko']
},
explanation: '"Nawala ko" bedeutet "Ich bin verloren" - wichtig, wenn du Hilfe brauchst.'
}
]
};
async function findOrCreateSystemUser() {
let systemUser = await User.findOne({
where: {
username: 'system'
}
});
if (!systemUser) {
systemUser = await User.findOne({
where: {
username: 'admin'
}
});
}
if (!systemUser) {
console.error('❌ System-Benutzer nicht gefunden.');
throw new Error('System user not found');
}
return systemUser;
}
async function updateSurvivalExercises() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
const systemUser = await findOrCreateSystemUser();
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
// Finde alle Bisaya-Kurse
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{
type: sequelize.QueryTypes.SELECT
}
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const courses = await sequelize.query(
`SELECT id, title, owner_user_id FROM community.vocab_course WHERE language_id = :languageId`,
{
replacements: { languageId: bisayaLanguage.id },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
let totalExercisesUpdated = 0;
let totalLessonsUpdated = 0;
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
// Finde "Überlebenssätze"-Lektionen
const lessons = await VocabCourseLesson.findAll({
where: {
courseId: course.id,
title: ['Überlebenssätze - Teil 1', 'Überlebenssätze - Teil 2']
},
order: [['lessonNumber', 'ASC']]
});
console.log(` ${lessons.length} "Überlebenssätze"-Lektionen gefunden\n`);
for (const lesson of lessons) {
const exercises = SURVIVAL_EXERCISES[lesson.title];
if (!exercises || exercises.length === 0) {
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`);
continue;
}
// Lösche bestehende Übungen
const deletedCount = await VocabGrammarExercise.destroy({
where: { lessonId: lesson.id }
});
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
// Erstelle neue Übungen
let exerciseNumber = 1;
for (const exerciseData of exercises) {
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: exerciseData.exerciseTypeId,
exerciseNumber: exerciseNumber++,
title: exerciseData.title,
instruction: exerciseData.instruction,
questionData: JSON.stringify(exerciseData.questionData),
answerData: JSON.stringify(exerciseData.answerData),
explanation: exerciseData.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalExercisesUpdated++;
}
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} neue Übung(en) erstellt`);
totalLessonsUpdated++;
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalLessonsUpdated} Lektionen aktualisiert`);
console.log(` ${totalExercisesUpdated} neue Grammatik-Übungen erstellt`);
}
updateSurvivalExercises()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env node
/**
* Script zum Aktualisieren der Woche-1-Lektionen in Bisaya-Kursen
*
* Verwendung:
* node backend/scripts/update-week1-bisaya-exercises.js
*
* - Entfernt alte Platzhalter-Übungen
* - Ersetzt durch korrekte Inhalte für "Woche 1 - Wiederholung" und "Woche 1 - Vokabeltest"
*/
import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import User from '../models/community/user.js';
const LESSON_TITLES = ['Woche 1 - Wiederholung', 'Woche 1 - Vokabeltest'];
const BISAYA_EXERCISES = {
'Woche 1 - Wiederholung': [
{ exerciseTypeId: 2, title: 'Wiederholung: Wie sagt man "Wie geht es dir?"?', instruction: 'Wähle die richtige Begrüßung aus.', questionData: { type: 'multiple_choice', question: 'Wie sagt man "Wie geht es dir?" auf Bisaya?', options: ['Kumusta ka?', 'Maayo', 'Salamat', 'Palihug'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Kumusta ka?" ist die Standard-Begrüßung auf Bisaya.' },
{ exerciseTypeId: 2, title: 'Wiederholung: Wie sagt man "Mutter" auf Bisaya?', instruction: 'Wähle die richtige Übersetzung.', questionData: { type: 'multiple_choice', question: 'Wie sagt man "Mutter" auf Bisaya?', options: ['Nanay', 'Tatay', 'Kuya', 'Ate'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Nanay" bedeutet "Mutter" auf Bisaya.' },
{ exerciseTypeId: 2, title: 'Wiederholung: Was bedeutet "Palangga taka"?', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Palangga taka"?', options: ['Ich hab dich lieb', 'Danke', 'Guten Tag', 'Auf Wiedersehen'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Palangga taka" bedeutet "Ich hab dich lieb" - wärmer als "I love you" im Familienkontext.' },
{ exerciseTypeId: 2, title: 'Wiederholung: Was fragt man mit "Nikaon ka?"?', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Nikaon ka?"?', options: ['Hast du schon gegessen?', 'Wie geht es dir?', 'Danke', 'Bitte'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Nikaon ka?" bedeutet "Hast du schon gegessen?" - typisch fürsorglich auf den Philippinen.' },
{ exerciseTypeId: 2, title: 'Wiederholung: Wie sagt man "Ich verstehe nicht"?', instruction: 'Wähle die richtige Übersetzung.', questionData: { type: 'multiple_choice', question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?', options: ['Wala ko kasabot', 'Salamat', 'Maayo', 'Palihug'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht".' }
],
'Woche 1 - Vokabeltest': [
{ exerciseTypeId: 2, title: 'Vokabeltest: Kumusta', instruction: 'Was bedeutet "Kumusta"?', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Kumusta"?', options: ['Wie geht es dir?', 'Danke', 'Bitte', 'Auf Wiedersehen'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Kumusta" kommt von spanisch "¿Cómo está?" - "Wie geht es dir?"' },
{ exerciseTypeId: 2, title: 'Vokabeltest: Lola', instruction: 'Wähle die richtige Übersetzung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Lola"?', options: ['Großmutter', 'Großvater', 'Mutter', 'Vater'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Lola" = Großmutter, "Lolo" = Großvater.' },
{ exerciseTypeId: 2, title: 'Vokabeltest: Salamat', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Salamat"?', options: ['Danke', 'Bitte', 'Entschuldigung', 'Gern geschehen'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Salamat" bedeutet "Danke".' },
{ exerciseTypeId: 2, title: 'Vokabeltest: Lami', instruction: 'Was bedeutet "Lami"?', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Lami"?', options: ['Lecker', 'Viel', 'Gut', 'Schnell'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Lami" bedeutet "lecker" oder "schmackhaft" - wichtig beim Essen!' },
{ exerciseTypeId: 2, title: 'Vokabeltest: Mingaw ko nimo', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Mingaw ko nimo"?', options: ['Ich vermisse dich', 'Ich freue mich', 'Ich mag dich', 'Ich liebe dich'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Mingaw ko nimo" bedeutet "Ich vermisse dich".' }
]
};
async function updateWeek1BisayaExercises() {
await sequelize.authenticate();
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
let systemUser;
try {
systemUser = await User.findOne({ where: { username: 'system' } });
if (!systemUser) systemUser = await User.findOne({ where: { username: 'admin' } });
if (!systemUser) throw new Error('System user not found');
} catch (e) {
console.error('❌ System-Benutzer nicht gefunden.');
throw e;
}
const [bisayaLanguage] = await sequelize.query(
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
{ type: sequelize.QueryTypes.SELECT }
);
if (!bisayaLanguage) {
console.error('❌ Bisaya-Sprache nicht gefunden.');
return;
}
const courses = await sequelize.query(
`SELECT c.id, c.title, c.owner_user_id
FROM community.vocab_course c
WHERE c.language_id = :languageId`,
{
replacements: { languageId: bisayaLanguage.id },
type: sequelize.QueryTypes.SELECT
}
);
console.log(`Gefunden: ${courses.length} Bisaya-Kurs(e)\n`);
let totalDeleted = 0;
let totalAdded = 0;
for (const course of courses) {
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
for (const lessonTitle of LESSON_TITLES) {
const exercises = BISAYA_EXERCISES[lessonTitle];
if (!exercises || exercises.length === 0) continue;
const lessons = await VocabCourseLesson.findAll({
where: { courseId: course.id, title: lessonTitle },
order: [['lessonNumber', 'ASC']]
});
for (const lesson of lessons) {
const deletedCount = await VocabGrammarExercise.destroy({
where: { lessonId: lesson.id }
});
totalDeleted += deletedCount;
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) entfernt`);
let exerciseNumber = 1;
for (const ex of exercises) {
await VocabGrammarExercise.create({
lessonId: lesson.id,
exerciseTypeId: ex.exerciseTypeId,
exerciseNumber: exerciseNumber++,
title: ex.title,
instruction: ex.instruction,
questionData: JSON.stringify(ex.questionData),
answerData: JSON.stringify(ex.answerData),
explanation: ex.explanation,
createdByUserId: course.owner_user_id || systemUser.id
});
totalAdded++;
}
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} neue Übung(en)`);
}
}
console.log('');
}
console.log(`\n🎉 Zusammenfassung:`);
console.log(` ${totalDeleted} Platzhalter-Übungen entfernt`);
console.log(` ${totalAdded} neue Übungen erstellt`);
}
updateWeek1BisayaExercises()
.then(() => {
sequelize.close();
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler:', error);
sequelize.close();
process.exit(1);
});

View File

@@ -3,6 +3,11 @@ import './config/loadEnv.js'; // .env deterministisch laden
import http from 'http'; import http from 'http';
import https from 'https'; import https from 'https';
import fs from 'fs'; import fs from 'fs';
// Assoziationen sofort setzen, bevor app (und damit Services/Router) geladen werden.
// So nutzen alle Modelle dieselbe Instanz inkl. Associations (verhindert EagerLoadingError).
import setupAssociations from './models/associations.js';
setupAssociations();
import app from './app.js'; import app from './app.js';
import { setupWebSocket } from './utils/socket.js'; import { setupWebSocket } from './utils/socket.js';
import { syncDatabase } from './utils/syncDatabase.js'; import { syncDatabase } from './utils/syncDatabase.js';

View File

@@ -0,0 +1,507 @@
import CalendarEvent from '../models/community/calendar_event.js';
import User from '../models/community/user.js';
import Friendship from '../models/community/friendship.js';
import UserParam from '../models/community/user_param.js';
import UserParamType from '../models/type/user_param.js';
import UserParamVisibility from '../models/community/user_param_visibility.js';
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
import { Op } from 'sequelize';
class CalendarService {
/**
* Get all calendar events for a user
* @param {string} hashedUserId - The user's hashed ID
* @param {object} options - Optional filters (startDate, endDate)
*/
async getEvents(hashedUserId, options = {}) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const where = { userId: user.id };
// Filter by date range if provided
if (options.startDate || options.endDate) {
where[Op.or] = [];
if (options.startDate && options.endDate) {
// Events that overlap with the requested range
where[Op.or].push({
startDate: { [Op.between]: [options.startDate, options.endDate] }
});
where[Op.or].push({
endDate: { [Op.between]: [options.startDate, options.endDate] }
});
where[Op.or].push({
[Op.and]: [
{ startDate: { [Op.lte]: options.startDate } },
{ endDate: { [Op.gte]: options.endDate } }
]
});
} else if (options.startDate) {
where[Op.or].push({ startDate: { [Op.gte]: options.startDate } });
where[Op.or].push({ endDate: { [Op.gte]: options.startDate } });
} else if (options.endDate) {
where[Op.or].push({ startDate: { [Op.lte]: options.endDate } });
}
}
const events = await CalendarEvent.findAll({
where,
order: [['startDate', 'ASC'], ['startTime', 'ASC']]
});
return events.map(e => this.formatEvent(e));
}
/**
* Get a single event by ID
*/
async getEvent(hashedUserId, eventId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const event = await CalendarEvent.findOne({
where: { id: eventId, userId: user.id }
});
if (!event) {
throw new Error('Event not found');
}
return this.formatEvent(event);
}
/**
* Create a new calendar event
*/
async createEvent(hashedUserId, eventData) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const event = await CalendarEvent.create({
userId: user.id,
title: eventData.title,
description: eventData.description || null,
categoryId: eventData.categoryId || 'personal',
startDate: eventData.startDate,
endDate: eventData.endDate || eventData.startDate,
startTime: eventData.allDay ? null : eventData.startTime,
endTime: eventData.allDay ? null : eventData.endTime,
allDay: eventData.allDay || false
});
return this.formatEvent(event);
}
/**
* Update an existing calendar event
*/
async updateEvent(hashedUserId, eventId, eventData) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const event = await CalendarEvent.findOne({
where: { id: eventId, userId: user.id }
});
if (!event) {
throw new Error('Event not found');
}
await event.update({
title: eventData.title,
description: eventData.description || null,
categoryId: eventData.categoryId || 'personal',
startDate: eventData.startDate,
endDate: eventData.endDate || eventData.startDate,
startTime: eventData.allDay ? null : eventData.startTime,
endTime: eventData.allDay ? null : eventData.endTime,
allDay: eventData.allDay || false
});
return this.formatEvent(event);
}
/**
* Delete a calendar event
*/
async deleteEvent(hashedUserId, eventId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const event = await CalendarEvent.findOne({
where: { id: eventId, userId: user.id }
});
if (!event) {
throw new Error('Event not found');
}
await event.destroy();
return { success: true };
}
/**
* Get friends' birthdays that are visible to the user
* @param {string} hashedUserId - The user's hashed ID
* @param {number} year - The year to get birthdays for
*/
async getFriendsBirthdays(hashedUserId, year) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
// Get user's age for visibility check
const userAge = await this.getUserAge(user.id);
// Get all accepted friendships
const friendships = await Friendship.findAll({
where: {
accepted: true,
withdrawn: false,
denied: false,
[Op.or]: [
{ user1Id: user.id },
{ user2Id: user.id }
]
}
});
const birthdays = [];
for (const friendship of friendships) {
// Get the friend's user ID
const friendId = friendship.user1Id === user.id ? friendship.user2Id : friendship.user1Id;
// Get the friend's birthdate param with visibility
const birthdateParam = await UserParam.findOne({
where: { userId: friendId },
include: [
{
model: UserParamType,
as: 'paramType',
where: { description: 'birthdate' }
},
{
model: UserParamVisibility,
as: 'param_visibilities',
include: [{
model: UserParamVisibilityType,
as: 'visibility_type'
}]
}
]
});
if (!birthdateParam || !birthdateParam.value) continue;
// Check visibility
const visibility = birthdateParam.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
if (!this.isBirthdayVisibleToFriend(visibility, userAge)) continue;
// Get friend's username
const friend = await User.findOne({
where: { id: friendId },
attributes: ['username', 'hashedId']
});
if (!friend) continue;
// Parse birthdate and create birthday event for the requested year
const birthdate = new Date(birthdateParam.value);
if (isNaN(birthdate.getTime())) continue;
const birthdayDate = `${year}-${String(birthdate.getMonth() + 1).padStart(2, '0')}-${String(birthdate.getDate()).padStart(2, '0')}`;
birthdays.push({
id: `birthday-${friend.hashedId}-${year}`,
title: friend.username,
categoryId: 'birthday',
startDate: birthdayDate,
endDate: birthdayDate,
allDay: true,
isBirthday: true,
friendHashedId: friend.hashedId
});
}
return birthdays;
}
/**
* Check if birthdate is visible to a friend
*/
isBirthdayVisibleToFriend(visibility, requestingUserAge) {
// Visible to friends if visibility is 'All', 'Friends', or 'FriendsAndAdults' (if adult)
return visibility === 'All' ||
visibility === 'Friends' ||
(visibility === 'FriendsAndAdults' && requestingUserAge >= 18);
}
/**
* Get user's age from birthdate
*/
async getUserAge(userId) {
const birthdateParam = await UserParam.findOne({
where: { userId },
include: [{
model: UserParamType,
as: 'paramType',
where: { description: 'birthdate' }
}]
});
if (!birthdateParam || !birthdateParam.value) return 0;
const birthdate = new Date(birthdateParam.value);
const today = new Date();
let age = today.getFullYear() - birthdate.getFullYear();
const monthDiff = today.getMonth() - birthdate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthdate.getDate())) {
age--;
}
return age;
}
/**
* Get upcoming birthdays for widget (sorted by next occurrence)
*/
async getUpcomingBirthdays(hashedUserId, limit = 10) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const userAge = await this.getUserAge(user.id);
const today = new Date();
const currentYear = today.getFullYear();
// Get all accepted friendships
const friendships = await Friendship.findAll({
where: {
accepted: true,
withdrawn: false,
denied: false,
[Op.or]: [
{ user1Id: user.id },
{ user2Id: user.id }
]
}
});
const birthdays = [];
for (const friendship of friendships) {
const friendId = friendship.user1Id === user.id ? friendship.user2Id : friendship.user1Id;
const birthdateParam = await UserParam.findOne({
where: { userId: friendId },
include: [
{
model: UserParamType,
as: 'paramType',
where: { description: 'birthdate' }
},
{
model: UserParamVisibility,
as: 'param_visibilities',
include: [{
model: UserParamVisibilityType,
as: 'visibility_type'
}]
}
]
});
if (!birthdateParam || !birthdateParam.value) continue;
const visibility = birthdateParam.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
if (!this.isBirthdayVisibleToFriend(visibility, userAge)) continue;
const friend = await User.findOne({
where: { id: friendId },
attributes: ['username', 'hashedId']
});
if (!friend) continue;
const birthdate = new Date(birthdateParam.value);
if (isNaN(birthdate.getTime())) continue;
// Calculate next birthday
let nextBirthday = new Date(currentYear, birthdate.getMonth(), birthdate.getDate());
if (nextBirthday < today) {
nextBirthday = new Date(currentYear + 1, birthdate.getMonth(), birthdate.getDate());
}
// Calculate days until birthday
const diffTime = nextBirthday.getTime() - today.getTime();
const daysUntil = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// Calculate age they will turn
const turningAge = nextBirthday.getFullYear() - birthdate.getFullYear();
birthdays.push({
username: friend.username,
hashedId: friend.hashedId,
date: `${String(birthdate.getMonth() + 1).padStart(2, '0')}-${String(birthdate.getDate()).padStart(2, '0')}`,
nextDate: nextBirthday.toISOString().split('T')[0],
daysUntil,
turningAge
});
}
// Sort by days until birthday
birthdays.sort((a, b) => a.daysUntil - b.daysUntil);
return birthdays.slice(0, limit);
}
/**
* Get upcoming events for widget
*/
async getUpcomingEvents(hashedUserId, limit = 10) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
const events = await CalendarEvent.findAll({
where: {
userId: user.id,
[Op.or]: [
{ startDate: { [Op.gte]: todayStr } },
{ endDate: { [Op.gte]: todayStr } }
]
},
order: [['startDate', 'ASC'], ['startTime', 'ASC']],
limit
});
return events.map(e => ({
id: e.id,
titel: e.title,
datum: e.startDate,
beschreibung: e.description || null,
categoryId: e.categoryId,
allDay: e.allDay,
startTime: e.startTime ? e.startTime.substring(0, 5) : null,
endDate: e.endDate
}));
}
/**
* Get mini calendar data for widget
*/
async getMiniCalendarData(hashedUserId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
// Get first and last day of month
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startStr = firstDay.toISOString().split('T')[0];
const endStr = lastDay.toISOString().split('T')[0];
// Get user events for this month
const events = await CalendarEvent.findAll({
where: {
userId: user.id,
[Op.or]: [
{ startDate: { [Op.between]: [startStr, endStr] } },
{ endDate: { [Op.between]: [startStr, endStr] } },
{
[Op.and]: [
{ startDate: { [Op.lte]: startStr } },
{ endDate: { [Op.gte]: endStr } }
]
}
]
}
});
// Get birthdays for this month
const birthdays = await this.getFriendsBirthdays(hashedUserId, year);
const monthBirthdays = birthdays.filter(b => {
const bMonth = parseInt(b.startDate.split('-')[1]);
return bMonth === month + 1;
});
// Build days with events
const daysWithEvents = {};
for (const event of events) {
const start = new Date(event.startDate);
const end = event.endDate ? new Date(event.endDate) : start;
for (let d = new Date(start); d <= end && d <= lastDay; d.setDate(d.getDate() + 1)) {
if (d >= firstDay) {
const dayNum = d.getDate();
if (!daysWithEvents[dayNum]) {
daysWithEvents[dayNum] = { events: 0, birthdays: 0 };
}
daysWithEvents[dayNum].events++;
}
}
}
for (const birthday of monthBirthdays) {
const dayNum = parseInt(birthday.startDate.split('-')[2]);
if (!daysWithEvents[dayNum]) {
daysWithEvents[dayNum] = { events: 0, birthdays: 0 };
}
daysWithEvents[dayNum].birthdays++;
}
return {
year,
month: month + 1,
today: today.getDate(),
firstDayOfWeek: firstDay.getDay() === 0 ? 7 : firstDay.getDay(), // Monday = 1
daysInMonth: lastDay.getDate(),
daysWithEvents
};
}
/**
* Format event for API response
*/
formatEvent(event) {
return {
id: event.id,
title: event.title,
description: event.description,
categoryId: event.categoryId,
startDate: event.startDate,
endDate: event.endDate,
startTime: event.startTime ? event.startTime.substring(0, 5) : null, // HH:MM format
endTime: event.endTime ? event.endTime.substring(0, 5) : null,
allDay: event.allDay,
createdAt: event.createdAt,
updatedAt: event.updatedAt
};
}
}
export default new CalendarService();

View File

@@ -0,0 +1,59 @@
import BaseService from './BaseService.js';
import UserDashboard from '../models/community/user_dashboard.js';
import WidgetType from '../models/type/widget_type.js';
class DashboardService extends BaseService {
/**
* Liste aller möglichen (verfügbaren) Widget-Typen.
* @returns {Promise<Array<{ id: number, label: string, endpoint: string, description: string|null, orderId: number }>>}
*/
async getAvailableWidgets() {
const rows = await WidgetType.findAll({
order: [['orderId', 'ASC'], ['id', 'ASC']],
attributes: ['id', 'label', 'endpoint', 'description', 'orderId']
});
return rows.map(r => ({
id: r.id,
label: r.label,
endpoint: r.endpoint,
description: r.description ?? null,
orderId: r.orderId
}));
}
/**
* @param {string} hashedUserId
* @returns {Promise<{ widgets: Array<{ id: string, title: string, endpoint: string }> }>}
*/
async getConfig(hashedUserId) {
const user = await this.getUserByHashedId(hashedUserId);
const row = await UserDashboard.findOne({ where: { userId: user.id } });
const config = row?.config ?? { widgets: [] };
if (!Array.isArray(config.widgets)) config.widgets = [];
return config;
}
/**
* @param {string} hashedUserId
* @param {{ widgets: Array<{ id: string, title: string, endpoint: string }> }} config
*/
async setConfig(hashedUserId, config) {
const user = await this.getUserByHashedId(hashedUserId);
const widgets = Array.isArray(config?.widgets) ? config.widgets : [];
const sanitized = widgets.map(w => ({
id: String(w?.id ?? ''),
title: String(w?.title ?? ''),
endpoint: String(w?.endpoint ?? '')
})).filter(w => w.id && (w.title || w.endpoint));
const payload = { widgets: sanitized };
const existing = await UserDashboard.findOne({ where: { userId: user.id } });
if (existing) {
await existing.update({ config: payload });
} else {
await UserDashboard.create({ userId: user.id, config: payload });
}
return { widgets: sanitized };
}
}
export default new DashboardService();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
/**
* Model-Proxy-Service: Lädt GLB-Dateien, komprimiert sie mit gltf-transform (Draco + Textur-Optimierung)
* und legt sie im Datei-Cache ab. Weitere Requests werden aus dem Cache bedient.
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { spawn } from 'child_process';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BACKEND_DIR = path.join(__dirname, '..');
const PROJECT_ROOT = path.join(BACKEND_DIR, '..');
const MODELS_REL = path.join('models', '3d', 'falukant', 'characters');
const DIST_MODELS = path.join(PROJECT_ROOT, 'frontend', 'dist', MODELS_REL);
const PUBLIC_MODELS = path.join(PROJECT_ROOT, 'frontend', 'public', MODELS_REL);
const CACHE_DIR = path.join(BACKEND_DIR, 'data', 'model-cache');
const CLI_PATH = path.join(BACKEND_DIR, 'node_modules', '.bin', 'gltf-transform');
/** Einmal ermitteltes Quellverzeichnis (frontend/dist oder frontend/public). */
let _sourceDir = null;
/** Production: frontend/dist; Local: frontend/public. Einmal pro Prozess festgelegt, damit
* isCacheValid() stets gegen dieselbe Quelle prüft (kein Wechsel zwischen dist/public). */
function getSourceDir() {
if (_sourceDir !== null) return _sourceDir;
_sourceDir = fs.existsSync(DIST_MODELS) ? DIST_MODELS : PUBLIC_MODELS;
return _sourceDir;
}
/** Erlaubte Dateinamen (nur [a-z0-9_.-]+.glb) */
const FILENAME_RE = /^[a-z0-9_.-]+\.glb$/i;
/** Laufende Optimierungen pro Dateiname → Promise<string> (Cache-Pfad) */
const pending = new Map();
/**
* Stellt sicher, dass der Cache-Ordner existiert.
*/
function ensureCacheDir() {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
}
/**
* Prüft, ob die Cache-Datei gültig ist (existiert und ist nicht älter als die Quelle).
* @param {string} sourcePath
* @param {string} cachePath
* @returns {boolean}
*/
function isCacheValid(sourcePath, cachePath) {
if (!fs.existsSync(cachePath)) return false;
if (!fs.existsSync(sourcePath)) return false;
const sourceStat = fs.statSync(sourcePath);
const cacheStat = fs.statSync(cachePath);
return cacheStat.mtimeMs >= sourceStat.mtimeMs;
}
/**
* Führt gltf-transform optimize aus (Draco + texture-size 1024).
* @param {string} inputPath
* @param {string} outputPath
* @returns {Promise<void>}
*/
function runOptimize(inputPath, outputPath) {
return new Promise((resolve, reject) => {
const child = spawn(
'node',
[CLI_PATH, 'optimize', inputPath, outputPath, '--compress', 'draco', '--texture-size', '1024'],
{ cwd: BACKEND_DIR, stdio: ['ignore', 'pipe', 'pipe'] }
);
let stderr = '';
child.stderr?.on('data', (d) => { stderr += d.toString(); });
child.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`gltf-transform exit ${code}: ${stderr}`));
});
child.on('error', reject);
});
}
/**
* Liefert den Pfad zur optimierten (gecachten) GLB-Datei.
* Erstellt die optimierte Datei per gltf-transform, falls nicht (gültig) gecacht.
*
* @param {string} filename - z.B. "male_child.glb"
* @returns {Promise<string>} Absoluter Pfad zur optimierten Datei (Cache)
* @throws {Error} Bei ungültigem Dateinamen oder fehlender Quelldatei
*/
export async function getOptimizedModelPath(filename) {
if (!FILENAME_RE.test(filename)) {
throw new Error(`Invalid model filename: ${filename}`);
}
const sourceDir = getSourceDir();
const sourcePath = path.join(sourceDir, filename);
const cacheFilename = filename.replace(/\.glb$/, '_opt.glb');
const cachePath = path.join(CACHE_DIR, cacheFilename);
if (!fs.existsSync(sourcePath)) {
throw new Error(`Source model not found: ${filename} (looked in ${sourceDir})`);
}
ensureCacheDir();
if (isCacheValid(sourcePath, cachePath)) {
return cachePath;
}
let promise = pending.get(filename);
if (!promise) {
promise = (async () => {
try {
await runOptimize(sourcePath, cachePath);
return cachePath;
} finally {
pending.delete(filename);
}
})();
pending.set(filename, promise);
}
return promise;
}

View File

@@ -0,0 +1,123 @@
/**
* Proxy für newsdata.io API.
* Endpoint: https://newsdata.io/api/1/news?apikey=...&language=...&category=...
* Pagination: counter = wievieltes Widget dieser Art (0 = erste Seite, 1 = zweite, …), damit News nicht doppelt gezeigt werden.
*/
const NEWS_BASE = 'https://newsdata.io/api/1/news';
// Cache für News-Ergebnisse (pro Sprache/Kategorie)
const newsCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten Cache
/**
* @param {object} options
* @param {number} options.counter - 0 = erste Seite, 1 = zweite, … (für Pagination/nextPage)
* @param {string} [options.language] - z. B. de, en
* @param {string} [options.category] - z. B. top, technology
* @returns {Promise<{ results: Array, nextPage: string|null }>}
*/
async function fetchNewsPage({ language = 'de', category = 'top', nextPageToken = null }) {
const apiKey = process.env.NEWSDATA_IO_API_KEY;
if (!apiKey || !apiKey.trim()) {
throw new Error('NEWSDATA_IO_API_KEY is not set in .env');
}
const params = new URLSearchParams();
params.set('apikey', apiKey.trim());
params.set('language', String(language));
params.set('category', String(category));
if (nextPageToken) params.set('page', nextPageToken);
const url = `${NEWS_BASE}?${params.toString()}`;
const res = await fetch(url);
if (!res.ok) {
const text = await res.text();
throw new Error(`newsdata.io: ${res.status} ${text.slice(0, 200)}`);
}
const data = await res.json();
return {
results: data.results ?? [],
nextPage: data.nextPage ?? null
};
}
/**
* Holt gecachte Artikel oder lädt sie von der API
*/
async function getCachedNews({ language = 'de', category = 'top', minArticles = 10 }) {
const cacheKey = `${language}:${category}`;
const cached = newsCache.get(cacheKey);
// Cache gültig?
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
// Wenn wir mehr Artikel brauchen, erweitern
if (cached.articles.length >= minArticles) {
return cached.articles;
}
}
// Neue Daten laden
const collected = cached?.articles || [];
let nextPageToken = cached?.nextPage || null;
while (collected.length < minArticles) {
try {
const page = await fetchNewsPage({
language,
category,
nextPageToken: nextPageToken || undefined
});
const items = page.results ?? [];
// Duplikate vermeiden (nach title)
const existingTitles = new Set(collected.map(a => a.title));
for (const item of items) {
if (!existingTitles.has(item.title)) {
collected.push(item);
existingTitles.add(item.title);
}
}
nextPageToken = page.nextPage ?? null;
if (items.length === 0 || !nextPageToken) break;
} catch (error) {
console.error('News fetch error:', error);
break;
}
}
// Cache aktualisieren
newsCache.set(cacheKey, {
articles: collected,
nextPage: nextPageToken,
timestamp: Date.now()
});
return collected;
}
/**
* Liefert den N-ten Artikel (counter = 0, 1, 2, …) für das N-te News-Widget.
* Nutzt Cache, damit mehrere Widgets unterschiedliche Artikel bekommen.
*
* @param {object} options
* @param {number} options.counter - Index des Artikels (0 = erster, 1 = zweiter, …)
* @param {string} [options.language]
* @param {string} [options.category]
* @returns {Promise<{ results: Array, nextPage: string|null }>}
*/
async function getNews({ counter = 0, language = 'de', category = 'top' }) {
const neededIndex = Math.max(0, counter);
// Mindestens so viele Artikel laden wie benötigt
const articles = await getCachedNews({
language,
category,
minArticles: neededIndex + 1
});
const single = articles[neededIndex] ? [articles[neededIndex]] : [];
return { results: single, nextPage: null };
}
export default { getNews };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
-- ============================================
-- Füge native_language_id zu vocab_course hinzu
-- ============================================
-- Dieses Feld speichert die Muttersprache des Lerners
-- z.B. "Bisaya für Deutschsprachige" -> language_id = Bisaya, native_language_id = Deutsch
-- Spalte hinzufügen
ALTER TABLE community.vocab_course
ADD COLUMN IF NOT EXISTS native_language_id INTEGER;
-- Foreign Key Constraint hinzufügen
ALTER TABLE community.vocab_course
ADD CONSTRAINT vocab_course_native_language_fk
FOREIGN KEY (native_language_id)
REFERENCES community.vocab_language(id)
ON DELETE SET NULL;
-- Index für bessere Performance
CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx
ON community.vocab_course(native_language_id);
-- Kommentar hinzufügen
COMMENT ON COLUMN community.vocab_course.native_language_id IS
'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".';

View File

@@ -0,0 +1,16 @@
-- ============================================
-- Neue Übungstypen für Sprachproduktion hinzufügen
-- ============================================
-- Führe diese Queries direkt auf dem Server aus
-- Neue Übungstypen hinzufügen
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('reading_aloud', 'Laut vorlesen - Übung zur Verbesserung der Aussprache'),
('speaking_from_memory', 'Aus dem Kopf sprechen - Übung zur aktiven Sprachproduktion')
ON CONFLICT (name) DO NOTHING;
-- ============================================
-- Hinweis:
-- - reading_aloud: Text wird angezeigt, User liest vor, Speech Recognition prüft
-- - speaking_from_memory: Prompt wird angezeigt, User spricht frei, manuelle/automatische Bewertung
-- ============================================

View File

@@ -0,0 +1,242 @@
-- ============================================
-- Vocab Courses - Vollständige SQL-Installation
-- ============================================
-- Führe diese Queries direkt auf dem Server aus
-- Reihenfolge beachten!
-- ============================================
-- 1. Kurs-Tabellen erstellen
-- ============================================
-- Kurs-Tabelle
CREATE TABLE IF NOT EXISTS community.vocab_course (
id SERIAL PRIMARY KEY,
owner_user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
language_id INTEGER NOT NULL,
difficulty_level INTEGER DEFAULT 1,
is_public BOOLEAN DEFAULT false,
share_code TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_owner_fk
FOREIGN KEY (owner_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
);
-- Lektionen innerhalb eines Kurses
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
id SERIAL PRIMARY KEY,
course_id INTEGER NOT NULL,
chapter_id INTEGER,
lesson_number INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
week_number INTEGER,
day_number INTEGER,
lesson_type TEXT DEFAULT 'vocab',
audio_url TEXT,
cultural_notes TEXT,
target_minutes INTEGER,
target_score_percent INTEGER DEFAULT 80,
requires_review BOOLEAN DEFAULT false,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_lesson_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_chapter_fk
FOREIGN KEY (chapter_id)
REFERENCES community.vocab_chapter(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
);
-- Einschreibungen in Kurse
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_enrollment_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
);
-- Fortschritt pro User und Lektion
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
lesson_id INTEGER NOT NULL,
completed BOOLEAN DEFAULT false,
score INTEGER DEFAULT 0,
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_course_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
);
-- ============================================
-- 2. Grammatik-Übungstabellen erstellen
-- ============================================
-- Grammatik-Übungstypen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
);
-- Grammatik-Übungen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
id SERIAL PRIMARY KEY,
lesson_id INTEGER NOT NULL,
exercise_type_id INTEGER NOT NULL,
exercise_number INTEGER NOT NULL,
title TEXT NOT NULL,
instruction TEXT,
question_data JSONB NOT NULL,
answer_data JSONB NOT NULL,
explanation TEXT,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_grammar_exercise_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_type_fk
FOREIGN KEY (exercise_type_id)
REFERENCES community.vocab_grammar_exercise_type(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
);
-- Fortschritt für Grammatik-Übungen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
exercise_id INTEGER NOT NULL,
attempts INTEGER DEFAULT 0,
correct_attempts INTEGER DEFAULT 0,
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
completed BOOLEAN DEFAULT false,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_grammar_exercise_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
FOREIGN KEY (exercise_id)
REFERENCES community.vocab_grammar_exercise(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
);
-- ============================================
-- 3. Indizes erstellen
-- ============================================
-- Kurs-Indizes
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
ON community.vocab_course(owner_user_id);
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
ON community.vocab_course(language_id);
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
ON community.vocab_course(is_public);
-- Lektion-Indizes
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
ON community.vocab_course_lesson(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
ON community.vocab_course_lesson(chapter_id);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
ON community.vocab_course_lesson(course_id, week_number);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
ON community.vocab_course_lesson(lesson_type);
-- Einschreibungs-Indizes
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
ON community.vocab_course_enrollment(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
ON community.vocab_course_enrollment(course_id);
-- Fortschritts-Indizes
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
ON community.vocab_course_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
ON community.vocab_course_progress(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
ON community.vocab_course_progress(lesson_id);
-- Grammatik-Übungs-Indizes
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
ON community.vocab_grammar_exercise(lesson_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
ON community.vocab_grammar_exercise(exercise_type_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
ON community.vocab_grammar_exercise_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
ON community.vocab_grammar_exercise_progress(exercise_id);
-- ============================================
-- 4. Standard-Daten einfügen
-- ============================================
-- Standard-Übungstypen für Grammatik
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('gap_fill', 'Lückentext-Übung'),
('multiple_choice', 'Multiple-Choice-Fragen'),
('sentence_building', 'Satzbau-Übung'),
('transformation', 'Satzumformung'),
('conjugation', 'Konjugations-Übung'),
('declension', 'Deklinations-Übung')
ON CONFLICT (name) DO NOTHING;
-- ============================================
-- 5. Kommentare hinzufügen (optional)
-- ============================================
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
'Type: vocab, grammar, conversation, culture, review';
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
'Zielzeit in Minuten für diese Lektion';
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
-- ============================================
-- Fertig!
-- ============================================
-- Alle Tabellen, Indizes und Standard-Daten wurden erstellt.
-- Du kannst jetzt Kurse erstellen und verwenden.

View File

@@ -0,0 +1,131 @@
-- ============================================
-- Vocab Courses - Update für bestehende Installation
-- ============================================
-- Führe diese Queries aus, wenn die Tabellen bereits existieren
-- (z.B. wenn nur die Basis-Tabellen erstellt wurden)
-- ============================================
-- 1. chapter_id optional machen
-- ============================================
ALTER TABLE community.vocab_course_lesson
ALTER COLUMN chapter_id DROP NOT NULL;
-- ============================================
-- 2. Neue Spalten zu vocab_course_lesson hinzufügen
-- ============================================
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS week_number INTEGER,
ADD COLUMN IF NOT EXISTS day_number INTEGER,
ADD COLUMN IF NOT EXISTS lesson_type TEXT DEFAULT 'vocab',
ADD COLUMN IF NOT EXISTS audio_url TEXT,
ADD COLUMN IF NOT EXISTS cultural_notes TEXT,
ADD COLUMN IF NOT EXISTS target_minutes INTEGER,
ADD COLUMN IF NOT EXISTS target_score_percent INTEGER DEFAULT 80,
ADD COLUMN IF NOT EXISTS requires_review BOOLEAN DEFAULT false;
-- ============================================
-- 3. Neue Indizes hinzufügen
-- ============================================
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
ON community.vocab_course_lesson(course_id, week_number);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
ON community.vocab_course_lesson(lesson_type);
-- ============================================
-- 4. Grammatik-Übungstabellen erstellen (falls noch nicht vorhanden)
-- ============================================
-- Grammatik-Übungstypen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
);
-- Grammatik-Übungen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
id SERIAL PRIMARY KEY,
lesson_id INTEGER NOT NULL,
exercise_type_id INTEGER NOT NULL,
exercise_number INTEGER NOT NULL,
title TEXT NOT NULL,
instruction TEXT,
question_data JSONB NOT NULL,
answer_data JSONB NOT NULL,
explanation TEXT,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_grammar_exercise_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_type_fk
FOREIGN KEY (exercise_type_id)
REFERENCES community.vocab_grammar_exercise_type(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
);
-- Fortschritt für Grammatik-Übungen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
exercise_id INTEGER NOT NULL,
attempts INTEGER DEFAULT 0,
correct_attempts INTEGER DEFAULT 0,
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
completed BOOLEAN DEFAULT false,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_grammar_exercise_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
FOREIGN KEY (exercise_id)
REFERENCES community.vocab_grammar_exercise(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
);
-- Indizes für Grammatik-Übungen
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
ON community.vocab_grammar_exercise(lesson_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
ON community.vocab_grammar_exercise(exercise_type_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
ON community.vocab_grammar_exercise_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
ON community.vocab_grammar_exercise_progress(exercise_id);
-- ============================================
-- 5. Standard-Daten einfügen
-- ============================================
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('gap_fill', 'Lückentext-Übung'),
('multiple_choice', 'Multiple-Choice-Fragen'),
('sentence_building', 'Satzbau-Übung'),
('transformation', 'Satzumformung'),
('conjugation', 'Konjugations-Übung'),
('declension', 'Deklinations-Übung')
ON CONFLICT (name) DO NOTHING;
-- ============================================
-- 6. Kommentare hinzufügen
-- ============================================
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
'Type: vocab, grammar, conversation, culture, review';
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
'Zielzeit in Minuten für diese Lektion';
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
-- ============================================
-- Fertig!
-- ============================================

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env node
/**
* Einfaches Script zum Erstellen/Aktualisieren von Tabellen
* Ohne Cleanup und Initialisierung
*/
import './config/loadEnv.js';
import { initializeDatabase, syncModelsAlways, sequelize } from './utils/sequelize.js';
import setupAssociations from './models/associations.js';
import models from './models/index.js';
console.log('🗄️ Starte Tabellen-Synchronisation (nur Schema-Updates)...');
async function main() {
try {
// 1. Datenbank-Schemas initialisieren
console.log('📊 Initialisiere Datenbank-Schemas...');
await initializeDatabase();
console.log('✅ Datenbank-Schemas initialisiert');
// 2. Associations setzen
console.log('🔗 Setze Associations...');
setupAssociations();
console.log('✅ Associations gesetzt');
// 3. Nur Tabellen synchronisieren (ohne Cleanup, ohne Initialisierung)
console.log('🔄 Synchronisiere Tabellen...');
await syncModelsAlways(models);
console.log('✅ Tabellen-Synchronisation erfolgreich abgeschlossen');
console.log('🎉 Tabellen-Synchronisation abgeschlossen!');
process.exit(0);
} catch (error) {
console.error('❌ Fehler bei der Tabellen-Synchronisation:', error);
console.error('Stack Trace:', error.stack);
process.exit(1);
}
}
// Script ausführen
main();

View File

@@ -282,7 +282,11 @@ async function initializeFalukantProducts() {
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 }, { labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 },
]; ];
const productsToInsert = baseProducts; const productsToInsert = baseProducts.map(p => ({
...p,
sellCostMinNeutral: Math.ceil(p.sellCost * factorMin),
sellCostMaxNeutral: Math.ceil(p.sellCost * factorMax),
}));
await ProductType.bulkCreate(productsToInsert, { await ProductType.bulkCreate(productsToInsert, {
ignoreDuplicates: true, ignoreDuplicates: true,

View File

@@ -16,6 +16,8 @@ import ReputationActionType from "../../models/falukant/type/reputation_action.j
import VehicleType from "../../models/falukant/type/vehicle.js"; import VehicleType from "../../models/falukant/type/vehicle.js";
import LearnRecipient from "../../models/falukant/type/learn_recipient.js"; import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js"; import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
import ChurchOfficeType from "../../models/falukant/type/church_office_type.js";
import ChurchOfficeRequirement from "../../models/falukant/predefine/church_office_requirement.js";
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js"; import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js"; import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
import UndergroundType from "../../models/falukant/type/underground.js"; import UndergroundType from "../../models/falukant/type/underground.js";
@@ -47,6 +49,8 @@ export const initializeFalukantTypes = async () => {
await initializePoliticalOfficeBenefitTypes(); await initializePoliticalOfficeBenefitTypes();
await initializePoliticalOfficeTypes(); await initializePoliticalOfficeTypes();
await initializePoliticalOfficePrerequisites(); await initializePoliticalOfficePrerequisites();
await initializeChurchOfficeTypes();
await initializeChurchOfficePrerequisites();
await initializeUndergroundTypes(); await initializeUndergroundTypes();
await initializeVehicleTypes(); await initializeVehicleTypes();
await initializeFalukantWeatherTypes(); await initializeFalukantWeatherTypes();
@@ -1024,6 +1028,136 @@ export const initializePoliticalOfficePrerequisites = async () => {
console.log(`[Falukant] OfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`); console.log(`[Falukant] OfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
}; };
// — Church Offices —
const churchOffices = [
{ tr: "lay-preacher", seatsPerRegion: 3, regionType: "city", hierarchyLevel: 0 },
{ tr: "village-priest", seatsPerRegion: 1, regionType: "city", hierarchyLevel: 1 },
{ tr: "parish-priest", seatsPerRegion: 1, regionType: "city", hierarchyLevel: 2 },
{ tr: "dean", seatsPerRegion: 1, regionType: "county", hierarchyLevel: 3 },
{ tr: "archdeacon", seatsPerRegion: 1, regionType: "shire", hierarchyLevel: 4 },
{ tr: "bishop", seatsPerRegion: 1, regionType: "markgravate", hierarchyLevel: 5 },
{ tr: "archbishop", seatsPerRegion: 1, regionType: "duchy", hierarchyLevel: 6 },
{ tr: "cardinal", seatsPerRegion: 3, regionType: "country", hierarchyLevel: 7 },
{ tr: "pope", seatsPerRegion: 1, regionType: "country", hierarchyLevel: 8 }
];
const churchOfficePrerequisites = [
{
officeTr: "lay-preacher",
prerequisite: {
prerequisiteOfficeTypeId: null // Einstiegsposition, keine Voraussetzung
}
},
{
officeTr: "village-priest",
prerequisite: {
prerequisiteOfficeTypeId: "lay-preacher"
}
},
{
officeTr: "parish-priest",
prerequisite: {
prerequisiteOfficeTypeId: "village-priest"
}
},
{
officeTr: "dean",
prerequisite: {
prerequisiteOfficeTypeId: "parish-priest"
}
},
{
officeTr: "archdeacon",
prerequisite: {
prerequisiteOfficeTypeId: "dean"
}
},
{
officeTr: "bishop",
prerequisite: {
prerequisiteOfficeTypeId: "archdeacon"
}
},
{
officeTr: "archbishop",
prerequisite: {
prerequisiteOfficeTypeId: "bishop"
}
},
{
officeTr: "cardinal",
prerequisite: {
prerequisiteOfficeTypeId: "archbishop"
}
},
{
officeTr: "pope",
prerequisite: {
prerequisiteOfficeTypeId: "cardinal"
}
}
];
export const initializeChurchOfficeTypes = async () => {
for (const co of churchOffices) {
await ChurchOfficeType.findOrCreate({
where: { name: co.tr },
defaults: {
seatsPerRegion: co.seatsPerRegion,
regionType: co.regionType,
hierarchyLevel: co.hierarchyLevel
}
});
}
console.log(`[Falukant] ChurchOfficeTypes initialized`);
};
export const initializeChurchOfficePrerequisites = async () => {
let created = 0;
let existing = 0;
let skipped = 0;
for (const prereq of churchOfficePrerequisites) {
const office = await ChurchOfficeType.findOne({ where: { name: prereq.officeTr } });
if (!office) { skipped++; continue; }
let prerequisiteOfficeTypeId = null;
if (prereq.prerequisite.prerequisiteOfficeTypeId) {
const prerequisiteOffice = await ChurchOfficeType.findOne({
where: { name: prereq.prerequisite.prerequisiteOfficeTypeId }
});
if (prerequisiteOffice) {
prerequisiteOfficeTypeId = prerequisiteOffice.id;
}
}
try {
const [record, wasCreated] = await ChurchOfficeRequirement.findOrCreate({
where: { officeTypeId: office.id },
defaults: {
officeTypeId: office.id,
prerequisiteOfficeTypeId: prerequisiteOfficeTypeId
}
});
if (wasCreated) {
created++;
} else {
// Aktualisiere, falls sich die Voraussetzung geändert hat
if (record.prerequisiteOfficeTypeId !== prerequisiteOfficeTypeId) {
await record.update({ prerequisiteOfficeTypeId: prerequisiteOfficeTypeId });
created++; // Zähle als Update
} else {
existing++;
}
}
} catch (e) {
if (falukantDebug) console.error('[Falukant] ChurchOfficePrereq Fehler', { officeId: office?.id, error: e.message });
throw e;
}
}
console.log(`[Falukant] ChurchOfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
};
export const initializeUndergroundTypes = async () => { export const initializeUndergroundTypes = async () => {
for (const underground of undergroundTypes) { for (const underground of undergroundTypes) {
await UndergroundType.findOrCreate({ await UndergroundType.findOrCreate({

View File

@@ -0,0 +1,30 @@
import WidgetType from '../models/type/widget_type.js';
const DEFAULT_WIDGET_TYPES = [
{ label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 },
{ label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 },
{ label: 'News', endpoint: '/api/news?language=de&category=top', description: 'Nachrichten (newsdata.io), Counter für Pagination', orderId: 3 },
{ label: 'Geburtstage', endpoint: '/api/calendar/widget/birthdays', description: 'Nächste Geburtstage von Freunden', orderId: 4 },
{ label: 'Nächste Termine', endpoint: '/api/calendar/widget/upcoming', description: 'Anstehende Kalendertermine', orderId: 5 },
{ label: 'Kalender', endpoint: '/api/calendar/widget/mini', description: 'Mini-Kalenderansicht', orderId: 6 }
];
/**
* Stellt die Standard-Widget-Typen in type.widget_type bereit.
* Idempotent: vorhandene Einträge werden nicht verändert.
*/
const initializeWidgetTypes = async () => {
for (const row of DEFAULT_WIDGET_TYPES) {
await WidgetType.findOrCreate({
where: { endpoint: row.endpoint },
defaults: {
label: row.label,
endpoint: row.endpoint,
description: row.description ?? null,
orderId: row.orderId ?? 0
}
});
}
};
export default initializeWidgetTypes;

View File

@@ -38,6 +38,12 @@ if (!dbName || !dbUser || !dbHost) {
throw new Error('Missing required database environment variables: DB_NAME, DB_USER, or DB_HOST'); throw new Error('Missing required database environment variables: DB_NAME, DB_USER, or DB_HOST');
} }
const poolMax = Number.parseInt(process.env.DB_POOL_MAX || '5', 10);
const poolMin = Number.parseInt(process.env.DB_POOL_MIN || '1', 10);
const poolAcquire = Number.parseInt(process.env.DB_POOL_ACQUIRE || '30000', 10);
const poolIdle = Number.parseInt(process.env.DB_POOL_IDLE || '10000', 10);
const poolEvict = Number.parseInt(process.env.DB_POOL_EVICT || '1000', 10);
const sequelize = new Sequelize(dbName, dbUser, dbPass, { const sequelize = new Sequelize(dbName, dbUser, dbPass, {
host: dbHost, host: dbHost,
dialect: 'postgres', dialect: 'postgres',
@@ -47,8 +53,55 @@ const sequelize = new Sequelize(dbName, dbUser, dbPass, {
}, },
benchmark: SQL_BENCHMARK, benchmark: SQL_BENCHMARK,
logging: sqlLogger, logging: sqlLogger,
pool: {
max: poolMax, // Maximale Anzahl von Verbindungen im Pool
min: poolMin, // Minimale Anzahl von Verbindungen im Pool
acquire: poolAcquire, // Maximale Zeit (ms) zum Erwerb einer Verbindung
idle: poolIdle, // Maximale Zeit (ms), die eine Verbindung idle sein kann, bevor sie entfernt wird
evict: poolEvict, // Intervall (ms) zum Prüfen auf idle Verbindungen
handleDisconnects: true // Automatisches Reconnect bei Verbindungsverlust
},
dialectOptions: {
connectTimeout: 30000 // Timeout für Verbindungsaufbau (30 Sekunden)
},
retry: {
max: 3, // Maximale Anzahl von Wiederholungsversuchen
match: [
/ConnectionError/,
/SequelizeConnectionError/,
/SequelizeConnectionRefusedError/,
/SequelizeHostNotFoundError/,
/SequelizeHostNotReachableError/,
/SequelizeInvalidConnectionError/,
/SequelizeConnectionTimedOutError/
]
}
}); });
// Helper: Query mit Timeout (muss nach sequelize Initialisierung definiert werden)
const queryWithTimeout = async (query, timeoutMs = 10000, description = 'Query', replacements = null) => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
});
try {
const queryOptions = replacements
? { replacements, type: sequelize.QueryTypes.SELECT }
: {};
const result = await Promise.race([
sequelize.query(query, queryOptions),
timeoutPromise
]);
return result;
} catch (error) {
if (error.message.includes('Timeout')) {
throw error; // Re-throw für bessere Fehlerbehandlung
}
throw error;
}
};
const createSchemas = async () => { const createSchemas = async () => {
await sequelize.query('CREATE SCHEMA IF NOT EXISTS community'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS community');
await sequelize.query('CREATE SCHEMA IF NOT EXISTS logs'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS logs');
@@ -435,12 +488,39 @@ async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, ch
} }
} }
// Helper: Sync mit Timeout
const syncModelWithTimeout = async (model, timeoutMs = 60000) => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Model sync timeout after ${timeoutMs}ms`)), timeoutMs);
});
try {
await Promise.race([
model.sync({ alter: true, force: false, constraints: false }),
timeoutPromise
]);
} catch (error) {
if (error.message.includes('timeout')) {
console.warn(` ⚠️ ${model.name} sync timeout nach ${timeoutMs}ms - überspringe...`);
return false;
}
throw error;
}
return true;
};
// Immer Schema-Updates (für Deployment) // Immer Schema-Updates (für Deployment)
const syncModelsAlways = async (models) => { const syncModelsAlways = async (models) => {
console.log('🔍 Deployment-Modus: Führe immer Schema-Updates durch...'); console.log('🔍 Deployment-Modus: Führe immer Schema-Updates durch...');
const modelArray = Object.values(models);
const totalModels = modelArray.length;
let currentModel = 0;
try { try {
for (const model of Object.values(models)) { for (const model of modelArray) {
currentModel++;
console.log(` 🔄 Syncing model ${model.name} (${currentModel}/${totalModels})...`);
// Temporarily remove VIRTUAL fields before sync to prevent sync errors // Temporarily remove VIRTUAL fields before sync to prevent sync errors
const originalAttributes = model.rawAttributes; const originalAttributes = model.rawAttributes;
const virtualFields = {}; const virtualFields = {};
@@ -520,72 +600,137 @@ const syncModelsAlways = async (models) => {
const schema = model.options?.schema || 'public'; const schema = model.options?.schema || 'public';
console.log(` 🔍 Checking for foreign keys in ${schema}.${tableName}...`); console.log(` 🔍 Checking for foreign keys in ${schema}.${tableName}...`);
const foreignKeys = await sequelize.query(` // Verwende queryWithTimeout für Foreign Key Queries
SELECT tc.constraint_name let foreignKeys = [];
FROM information_schema.table_constraints AS tc try {
WHERE tc.constraint_type = 'FOREIGN KEY' // Verwende direkte SQL-String-Interpolation, da Parameter-Binding bei queryWithTimeout Probleme macht
AND tc.table_name = :tableName const result = await queryWithTimeout(`
AND tc.table_schema = :schema SELECT tc.constraint_name
`, { FROM information_schema.table_constraints AS tc
replacements: { tableName, schema }, WHERE tc.constraint_type = 'FOREIGN KEY'
type: sequelize.QueryTypes.SELECT AND tc.table_name = '${tableName.replace(/'/g, "''")}'
}); AND tc.table_schema = '${schema.replace(/'/g, "''")}'
`, 10000, `FK check for ${model.name}`);
// Result ist ein Array von Objekten
foreignKeys = Array.isArray(result) ? result : [];
} catch (fkCheckError) {
if (fkCheckError.message && fkCheckError.message.includes('Timeout')) {
console.warn(` ⚠️ Timeout beim Prüfen der Foreign Keys für ${model.name} - überspringe FK-Check...`);
foreignKeys = [];
} else {
// Bei anderen Fehlern auch überspringen, nicht kritisch
console.warn(` ⚠️ Fehler beim Prüfen der Foreign Keys für ${model.name}:`, fkCheckError.message?.substring(0, 100));
foreignKeys = [];
}
}
if (foreignKeys && foreignKeys.length > 0) { if (foreignKeys && foreignKeys.length > 0) {
console.log(` ⚠️ Found ${foreignKeys.length} existing foreign keys:`, foreignKeys.map(fk => fk.constraint_name).join(', ')); const constraintNames = foreignKeys
console.log(` ⚠️ Removing ${foreignKeys.length} existing foreign keys from ${model.name} (schema: ${schema}) before sync`); .map(fk => fk?.constraint_name)
for (const fk of foreignKeys) { .filter(name => name && name !== 'undefined' && name !== undefined);
console.log(` 🗑️ Dropping constraint: ${fk.constraint_name}`);
await sequelize.query(` if (constraintNames.length > 0) {
ALTER TABLE "${schema}"."${tableName}" console.log(` ⚠️ Found ${constraintNames.length} existing foreign keys:`, constraintNames.join(', '));
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE console.log(` ⚠️ Removing ${constraintNames.length} existing foreign keys from ${model.name} (schema: ${schema}) before sync`);
`); for (const constraintName of constraintNames) {
try {
console.log(` 🗑️ Dropping constraint: ${constraintName}`);
await queryWithTimeout(`
ALTER TABLE "${schema}"."${tableName}"
DROP CONSTRAINT IF EXISTS "${constraintName.replace(/"/g, '""')}" CASCADE
`, 10000, `Drop FK ${constraintName}`);
} catch (dropError) {
if (dropError.message && dropError.message.includes('Timeout')) {
console.warn(` ⚠️ Timeout beim Entfernen von ${constraintName} - überspringe...`);
} else {
console.warn(` ⚠️ Konnte ${constraintName} nicht entfernen:`, dropError.message?.substring(0, 100));
}
}
}
console.log(` ✅ Foreign key removal completed for ${model.name}`);
} else {
console.log(` ⚠️ Foreign keys gefunden, aber constraint_name ist undefined - überspringe FK-Entfernung`);
} }
console.log(` ✅ All foreign keys removed for ${model.name}`);
} else { } else {
console.log(` ✅ No foreign keys found for ${model.name}`); console.log(` ✅ No foreign keys found for ${model.name}`);
} }
} catch (fkError) { } catch (fkError) {
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message); // Ignoriere Timeout-Fehler - nicht kritisch
console.warn(` ⚠️ Error details:`, fkError); if (fkError.message && fkError.message.includes('Timeout')) {
console.warn(` ⚠️ Timeout beim Prüfen der Foreign Keys für ${model.name} - überspringe...`);
} else {
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message?.substring(0, 100));
}
} }
console.log(` 🔄 Syncing model ${model.name} with constraints: false`); console.log(` 🔄 Syncing model ${model.name} with constraints: false`);
try { try {
// Versuche doppelte pg_description Einträge vor dem Sync zu bereinigen // Überspringe pg_description Cleanup komplett - benötigt Superuser-Rechte und blockiert oft
// Hinweis: Benötigt Superuser-Rechte oder spezielle Berechtigungen // Diese Query ist nicht kritisch für die Funktionalität
// Prüfe ob Tabelle bereits existiert - wenn ja, überspringe Sync für große Tabellen
const tableName = model.tableName;
const schema = model.options?.schema || 'public';
let tableExists = false;
try { try {
const tableName = model.tableName; const existsResult = await queryWithTimeout(`
const schema = model.options?.schema || 'public'; SELECT EXISTS (
// Verwende direkte Parameter-Einsetzung, da DO $$ keine Parameterbindung unterstützt SELECT 1 FROM information_schema.tables
// Die Parameter sind sicher, da sie von Sequelize-Modell-Eigenschaften kommen WHERE table_schema = :schema
await sequelize.query(` AND table_name = :tableName
DELETE FROM pg_catalog.pg_description d1 ) as exists
WHERE d1.objoid IN ( `, 5000, `Table exists check for ${model.name}`, { schema, tableName });
SELECT c.oid tableExists = existsResult && existsResult[0] && existsResult[0].exists;
FROM pg_catalog.pg_class c } catch (checkError) {
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace // Bei Fehler annehmen, dass Tabelle nicht existiert und Sync versuchen
WHERE c.relname = '${tableName.replace(/'/g, "''")}' tableExists = false;
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 }); // Für große Tabellen (FalukantUser, FalukantCharacter) - wenn Tabelle existiert, überspringe Sync
const largeTables = ['FalukantUser', 'FalukantCharacter', 'Notification', 'RegionData'];
if (largeTables.includes(model.name) && tableExists) {
console.log(` ${model.name} Tabelle existiert bereits - überspringe Sync (zu groß für alter)`);
// Restore associations before continuing
if (associationKeys.length > 0) {
model.associations = originalAssociations;
}
// Restore virtual fields
for (const [key, attr] of Object.entries(virtualFields)) {
model.rawAttributes[key] = attr;
}
continue;
}
// Für Tabellen mit problematischen ENUM-Änderungen - wenn Tabelle existiert, überspringe Sync
// Sequelize generiert fehlerhaftes SQL bei ENUM-Änderungen mit Kommentaren
const enumProblemTables = ['TaxiMapTileHouse'];
if (enumProblemTables.includes(model.name) && tableExists) {
console.log(` ${model.name} Tabelle existiert bereits - überspringe Sync (ENUM-Änderungen problematisch)`);
// Restore associations before continuing
if (associationKeys.length > 0) {
model.associations = originalAssociations;
}
// Restore virtual fields
for (const [key, attr] of Object.entries(virtualFields)) {
model.rawAttributes[key] = attr;
}
continue;
}
// Verwende syncModelWithTimeout für große Tabellen
const syncSuccess = await syncModelWithTimeout(model, 60000);
if (!syncSuccess) {
console.warn(` ⚠️ ${model.name} wurde übersprungen aufgrund von Timeout`);
// Restore associations before continuing
if (associationKeys.length > 0) {
model.associations = originalAssociations;
}
// Restore virtual fields
for (const [key, attr] of Object.entries(virtualFields)) {
model.rawAttributes[key] = attr;
}
continue;
}
} catch (syncError) { } catch (syncError) {
// Wenn Sequelize einen "mehr als eine Zeile" Fehler hat, überspringe das Model // 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 // Dies kann durch doppelte pg_description Einträge oder mehrere Tabellen mit demselben Namen verursacht werden

View File

@@ -1,6 +1,64 @@
// syncDatabase.js // syncDatabase.js
import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways, sequelize } from './sequelize.js'; import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways, sequelize } from './sequelize.js';
// Helper: Query mit Timeout
const queryWithTimeout = async (query, timeoutMs = 30000, description = 'Query') => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
});
try {
const result = await Promise.race([
sequelize.query(query),
timeoutPromise
]);
return result;
} catch (error) {
if (error.message.includes('Timeout')) {
console.warn(`⚠️ ${description} hat Timeout nach ${timeoutMs}ms - überspringe...`);
return [null, 0]; // Return empty result
}
throw error;
}
};
// Helper: Retry wrapper for transient pool/connection issues
const runWithRetry = async (fn, { retries = 3, delayMs = 2000, description = 'operation' } = {}) => {
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
const isAcquireTimeout = error?.name === 'SequelizeConnectionAcquireTimeoutError'
|| error?.message?.includes('ConnectionAcquireTimeoutError')
|| error?.message?.includes('Operation timeout');
if (!isAcquireTimeout || attempt === retries) {
throw error;
}
console.warn(`⚠️ ${description} fehlgeschlagen (AcquireTimeout). Retry ${attempt}/${retries} in ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw lastError;
};
// Helper: Prüft ob Tabelle existiert
const tableExists = async (schema, tableName) => {
try {
const result = await sequelize.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '${schema}'
AND table_name = '${tableName}'
);
`, { type: sequelize.QueryTypes.SELECT });
return result[0]?.exists || false;
} catch (error) {
return false;
}
};
import initializeTypes from './initializeTypes.js'; import initializeTypes from './initializeTypes.js';
import initializeSettings from './initializeSettings.js'; import initializeSettings from './initializeSettings.js';
import initializeUserRights from './initializeUserRights.js'; import initializeUserRights from './initializeUserRights.js';
@@ -14,6 +72,7 @@ import initializeChat from './initializeChat.js';
import initializeMatch3Data from './initializeMatch3.js'; import initializeMatch3Data from './initializeMatch3.js';
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js'; import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
import initializeTaxi from './initializeTaxi.js'; import initializeTaxi from './initializeTaxi.js';
import initializeWidgetTypes from './initializeWidgetTypes.js';
// Normale Synchronisation (nur bei STAGE=dev Schema-Updates) // Normale Synchronisation (nur bei STAGE=dev Schema-Updates)
const syncDatabase = async () => { const syncDatabase = async () => {
@@ -31,7 +90,40 @@ const syncDatabase = async () => {
} }
console.log("Initializing database schemas..."); console.log("Initializing database schemas...");
await initializeDatabase(); await initializeDatabase();
// Dashboard: Widget-Typen-Tabelle (mögliche Widgets)
console.log("Ensuring widget_type table exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS type.widget_type (
id SERIAL PRIMARY KEY,
label VARCHAR(255) NOT NULL,
endpoint VARCHAR(255) NOT NULL,
description VARCHAR(255),
order_id INTEGER NOT NULL DEFAULT 0
);
`);
} catch (e) {
console.warn('⚠️ Konnte type.widget_type nicht anlegen:', e?.message || e);
}
// Dashboard: Benutzer-Konfiguration (Widgets pro User)
console.log("Ensuring user_dashboard table exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS community.user_dashboard (
user_id INTEGER NOT NULL PRIMARY KEY,
config JSONB NOT NULL DEFAULT '{"widgets":[]}'::jsonb,
CONSTRAINT user_dashboard_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE
);
`);
} catch (e) {
console.warn('⚠️ Konnte community.user_dashboard nicht anlegen:', e?.message || e);
}
// Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations) // Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations)
// Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt. // Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt.
@@ -144,12 +236,330 @@ const syncDatabase = async () => {
ON community.vocab_chapter_lexeme(learning_lexeme_id); ON community.vocab_chapter_lexeme(learning_lexeme_id);
CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx
ON community.vocab_chapter_lexeme(reference_lexeme_id); ON community.vocab_chapter_lexeme(reference_lexeme_id);
// Kurs-Tabellen
CREATE TABLE IF NOT EXISTS community.vocab_course (
id SERIAL PRIMARY KEY,
owner_user_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
language_id INTEGER NOT NULL,
native_language_id INTEGER,
difficulty_level INTEGER DEFAULT 1,
is_public BOOLEAN DEFAULT false,
share_code TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_owner_fk
FOREIGN KEY (owner_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_language_fk
FOREIGN KEY (language_id)
REFERENCES community.vocab_language(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_native_language_fk
FOREIGN KEY (native_language_id)
REFERENCES community.vocab_language(id)
ON DELETE SET NULL,
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
);
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
id SERIAL PRIMARY KEY,
course_id INTEGER NOT NULL,
chapter_id INTEGER,
lesson_number INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
week_number INTEGER,
day_number INTEGER,
lesson_type TEXT DEFAULT 'vocab',
audio_url TEXT,
cultural_notes TEXT,
target_minutes INTEGER,
target_score_percent INTEGER DEFAULT 80,
requires_review BOOLEAN DEFAULT false,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_lesson_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_chapter_fk
FOREIGN KEY (chapter_id)
REFERENCES community.vocab_chapter(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
);
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_course_enrollment_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
);
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
lesson_id INTEGER NOT NULL,
completed BOOLEAN DEFAULT false,
score INTEGER DEFAULT 0,
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_course_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_course_fk
FOREIGN KEY (course_id)
REFERENCES community.vocab_course(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
);
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
ON community.vocab_course(owner_user_id);
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
ON community.vocab_course(language_id);
CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx
ON community.vocab_course(native_language_id);
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
ON community.vocab_course(is_public);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
ON community.vocab_course_lesson(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
ON community.vocab_course_lesson(chapter_id);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
ON community.vocab_course_lesson(course_id, week_number);
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
ON community.vocab_course_lesson(lesson_type);
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
ON community.vocab_course_enrollment(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
ON community.vocab_course_enrollment(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
ON community.vocab_course_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
ON community.vocab_course_progress(course_id);
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
ON community.vocab_course_progress(lesson_id);
// Grammatik-Übungstypen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
);
// Grammatik-Übungen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
id SERIAL PRIMARY KEY,
lesson_id INTEGER NOT NULL,
exercise_type_id INTEGER NOT NULL,
exercise_number INTEGER NOT NULL,
title TEXT NOT NULL,
instruction TEXT,
question_data JSONB NOT NULL,
answer_data JSONB NOT NULL,
explanation TEXT,
created_by_user_id INTEGER NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_grammar_exercise_lesson_fk
FOREIGN KEY (lesson_id)
REFERENCES community.vocab_course_lesson(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_type_fk
FOREIGN KEY (exercise_type_id)
REFERENCES community.vocab_grammar_exercise_type(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_creator_fk
FOREIGN KEY (created_by_user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
);
// Fortschritt für Grammatik-Übungen
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
exercise_id INTEGER NOT NULL,
attempts INTEGER DEFAULT 0,
correct_attempts INTEGER DEFAULT 0,
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
completed BOOLEAN DEFAULT false,
completed_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT vocab_grammar_exercise_progress_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
FOREIGN KEY (exercise_id)
REFERENCES community.vocab_grammar_exercise(id)
ON DELETE CASCADE,
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
ON community.vocab_grammar_exercise(lesson_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
ON community.vocab_grammar_exercise(exercise_type_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
ON community.vocab_grammar_exercise_progress(user_id);
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
ON community.vocab_grammar_exercise_progress(exercise_id);
-- Standard-Übungstypen einfügen
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('gap_fill', 'Lückentext-Übung'),
('multiple_choice', 'Multiple-Choice-Fragen'),
('sentence_building', 'Satzbau-Übung'),
('transformation', 'Satzumformung'),
('conjugation', 'Konjugations-Übung'),
('declension', 'Deklinations-Übung')
ON CONFLICT (name) DO NOTHING;
`); `);
console.log("✅ Vocab-Trainer Tabellen sind vorhanden."); console.log("✅ Vocab-Trainer Tabellen sind vorhanden.");
console.log("✅ Vocab-Course Tabellen sind vorhanden.");
console.log("✅ Vocab-Grammar-Exercise Tabellen sind vorhanden.");
} catch (e) { } catch (e) {
console.warn('⚠️ Konnte Vocab-Trainer Tabellen nicht sicherstellen:', e?.message || e); console.warn('⚠️ Konnte Vocab-Trainer Tabellen nicht sicherstellen:', e?.message || e);
} }
// Migration: ChurchApplication supervisor_id nullable machen (kritisch für Funktionalität)
console.log("Making church_application supervisor_id nullable...");
try {
await sequelize.query(`
DO $$
BEGIN
-- Prüfe ob supervisor_id NOT NULL Constraint existiert
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'church_application'
AND column_name = 'supervisor_id'
AND is_nullable = 'NO'
) THEN
ALTER TABLE falukant_data.church_application
ALTER COLUMN supervisor_id DROP NOT NULL;
RAISE NOTICE 'supervisor_id NOT NULL Constraint entfernt';
END IF;
END
$$;
`);
console.log("✅ church_application supervisor_id ist jetzt nullable");
} catch (e) {
console.warn('⚠️ Konnte church_application supervisor_id nicht nullable machen:', e?.message || e);
}
// Relationship-/Marriage-Proposal-Änderungen loggen (keine Einträge löschen; ohne db:migrate)
console.log("Ensuring relationship change log (falukant) exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_log.relationship_change_log (
id serial PRIMARY KEY,
changed_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
table_name character varying(64) NOT NULL,
operation character varying(16) NOT NULL,
record_id integer,
payload_old jsonb,
payload_new jsonb
);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS relationship_change_log_changed_at_idx
ON falukant_log.relationship_change_log (changed_at);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS relationship_change_log_table_operation_idx
ON falukant_log.relationship_change_log (table_name, operation);
`);
await sequelize.query(`
CREATE OR REPLACE FUNCTION falukant_log.log_relationship_change()
RETURNS TRIGGER AS $$
DECLARE
v_record_id INTEGER;
v_payload_old JSONB;
v_payload_new JSONB;
BEGIN
IF TG_OP = 'INSERT' THEN
v_record_id := NEW.id;
v_payload_old := NULL;
v_payload_new := to_jsonb(NEW);
ELSIF TG_OP = 'UPDATE' THEN
v_record_id := NEW.id;
v_payload_old := to_jsonb(OLD);
v_payload_new := to_jsonb(NEW);
ELSIF TG_OP = 'DELETE' THEN
v_record_id := OLD.id;
v_payload_old := to_jsonb(OLD);
v_payload_new := NULL;
END IF;
INSERT INTO falukant_log.relationship_change_log (
table_name, operation, record_id, payload_old, payload_new
) VALUES (
TG_TABLE_NAME, TG_OP, v_record_id, v_payload_old, v_payload_new
);
IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
END;
$$ LANGUAGE plpgsql;
`);
await sequelize.query(`
DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.relationship;
CREATE TRIGGER trg_log_relationship_change
AFTER INSERT OR UPDATE OR DELETE ON falukant_data.relationship
FOR EACH ROW
EXECUTE FUNCTION falukant_log.log_relationship_change();
`);
await sequelize.query(`
DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.marriage_proposals;
CREATE TRIGGER trg_log_relationship_change
AFTER INSERT OR UPDATE OR DELETE ON falukant_data.marriage_proposals
FOR EACH ROW
EXECUTE FUNCTION falukant_log.log_relationship_change();
`);
console.log("✅ relationship_change_log und Trigger sind vorhanden.");
} catch (e) {
console.warn('⚠️ relationship_change_log/Trigger konnten nicht sichergestellt werden:', e?.message || e);
}
// Preishistorie für Produkte (Zeitreihe) nur Schema/Struktur, noch ohne Logik
console.log("Ensuring falukant_log.product_price_history exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_log.product_price_history (
id serial PRIMARY KEY,
product_id integer NOT NULL,
region_id integer NOT NULL,
price numeric(12,2) NOT NULL,
recorded_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS product_price_history_product_region_recorded_idx
ON falukant_log.product_price_history (product_id, region_id, recorded_at);
`);
console.log("✅ product_price_history ist vorhanden.");
} catch (e) {
console.warn('⚠️ product_price_history konnte nicht sichergestellt werden:', e?.message || e);
}
// Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt // Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt
console.log("Pre-ensure Taxi columns (traffic_light) ..."); console.log("Pre-ensure Taxi columns (traffic_light) ...");
try { try {
@@ -277,14 +687,17 @@ const syncDatabase = async () => {
} }
} }
console.log("Setting up associations..."); // Associations werden vom Aufrufer (z. B. server.js) vor dem Aufruf von syncDatabase() gesetzt.
setupAssociations(); // Kein setupAssociations() hier, sonst doppelter Aufruf → AssociationError (alias "rooms" etc.).
console.log("Synchronizing models..."); console.log("Synchronizing models...");
await syncModelsWithUpdates(models); await syncModelsWithUpdates(models);
console.log("Initializing settings..."); console.log("Initializing settings...");
await initializeSettings(); await runWithRetry(
() => initializeSettings(),
{ retries: 3, delayMs: 2000, description: 'initializeSettings' }
);
console.log("Initializing types..."); console.log("Initializing types...");
await initializeTypes(); await initializeTypes();
@@ -318,6 +731,9 @@ const syncDatabase = async () => {
console.log("Initializing Taxi..."); console.log("Initializing Taxi...");
await initializeTaxi(); await initializeTaxi();
console.log("Initializing widget types...");
await initializeWidgetTypes();
console.log('Database synchronization complete.'); console.log('Database synchronization complete.');
} catch (error) { } catch (error) {
console.error('Unable to synchronize the database:', error); console.error('Unable to synchronize the database:', error);
@@ -399,89 +815,122 @@ const syncDatabaseForDeployment = async () => {
console.warn('⚠️ Konnte Transport-Spalten nicht nullable machen:', e?.message || e); console.warn('⚠️ Konnte Transport-Spalten nicht nullable machen:', e?.message || e);
} }
// Migration: ChurchApplication supervisor_id nullable machen
console.log("Making church_application supervisor_id nullable...");
try {
await sequelize.query(`
DO $$
BEGIN
-- Prüfe ob supervisor_id NOT NULL Constraint existiert
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'church_application'
AND column_name = 'supervisor_id'
AND is_nullable = 'NO'
) THEN
ALTER TABLE falukant_data.church_application
ALTER COLUMN supervisor_id DROP NOT NULL;
RAISE NOTICE 'supervisor_id NOT NULL Constraint entfernt';
END IF;
END
$$;
`);
console.log("✅ church_application supervisor_id ist jetzt nullable");
} catch (e) {
console.warn('⚠️ Konnte church_application supervisor_id nicht nullable machen:', e?.message || e);
}
// Cleanup: Entferne verwaiste Einträge vor Schema-Updates // Cleanup: Entferne verwaiste Einträge vor Schema-Updates
console.log("Cleaning up orphaned entries..."); console.log("Cleaning up orphaned entries...");
try { try {
// Cleanup user_param_visibility // Cleanup user_param_visibility (optimiert mit LEFT JOIN)
const result1 = await sequelize.query(` console.log(" → Prüfe user_param_visibility...");
const result1 = await queryWithTimeout(`
DELETE FROM community.user_param_visibility DELETE FROM community.user_param_visibility
WHERE param_id NOT IN ( WHERE param_id NOT IN (
SELECT id FROM community.user_param SELECT id FROM community.user_param
); );
`); `, 30000, 'user_param_visibility cleanup');
const deletedCount1 = result1[1] || 0; const deletedCount1 = result1[1] || 0;
if (deletedCount1 > 0) { if (deletedCount1 > 0) {
console.log(`${deletedCount1} verwaiste user_param_visibility Einträge entfernt`); console.log(`${deletedCount1} verwaiste user_param_visibility Einträge entfernt`);
} }
// Cleanup stock mit ungültigen branch_id (0 oder nicht existierend) // Cleanup stock mit ungültigen branch_id (0 oder nicht existierend)
const result2 = await sequelize.query(` console.log(" → Prüfe stock...");
const result2 = await queryWithTimeout(`
DELETE FROM falukant_data.stock DELETE FROM falukant_data.stock
WHERE branch_id = 0 OR branch_id NOT IN ( WHERE branch_id = 0 OR branch_id NOT IN (
SELECT id FROM falukant_data.branch SELECT id FROM falukant_data.branch
); );
`); `, 30000, 'stock cleanup');
const deletedCount2 = result2[1] || 0; const deletedCount2 = result2[1] || 0;
if (deletedCount2 > 0) { if (deletedCount2 > 0) {
console.log(`${deletedCount2} verwaiste stock Einträge entfernt`); console.log(`${deletedCount2} verwaiste stock Einträge entfernt`);
} }
// Cleanup knowledge mit ungültigen character_id oder product_id // Cleanup knowledge mit ungültigen character_id oder product_id
const result3 = await sequelize.query(` console.log(" → Prüfe knowledge...");
const result3 = await queryWithTimeout(`
DELETE FROM falukant_data.knowledge DELETE FROM falukant_data.knowledge
WHERE character_id NOT IN ( WHERE character_id NOT IN (
SELECT id FROM falukant_data.character SELECT id FROM falukant_data.character
) OR product_id NOT IN ( ) OR product_id NOT IN (
SELECT id FROM falukant_type.product SELECT id FROM falukant_type.product
); );
`); `, 30000, 'knowledge cleanup');
const deletedCount3 = result3[1] || 0; const deletedCount3 = result3[1] || 0;
if (deletedCount3 > 0) { if (deletedCount3 > 0) {
console.log(`${deletedCount3} verwaiste knowledge Einträge entfernt`); console.log(`${deletedCount3} verwaiste knowledge Einträge entfernt`);
} }
// Cleanup notification mit ungültigen user_id // Cleanup notification mit ungültigen user_id
const result4 = await sequelize.query(` console.log(" → Prüfe notification...");
const result4 = await queryWithTimeout(`
DELETE FROM falukant_log.notification DELETE FROM falukant_log.notification
WHERE user_id NOT IN ( WHERE user_id NOT IN (
SELECT id FROM falukant_data.falukant_user SELECT id FROM falukant_data.falukant_user
); );
`); `, 30000, 'notification cleanup');
const deletedCount4 = result4[1] || 0; const deletedCount4 = result4[1] || 0;
if (deletedCount4 > 0) { if (deletedCount4 > 0) {
console.log(`${deletedCount4} verwaiste notification Einträge entfernt`); console.log(`${deletedCount4} verwaiste notification Einträge entfernt`);
} }
// Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id // Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id
const result5 = await sequelize.query(` console.log(" → Prüfe promotional_gift...");
const result5 = await queryWithTimeout(`
DELETE FROM falukant_log.promotional_gift DELETE FROM falukant_log.promotional_gift
WHERE sender_character_id NOT IN ( WHERE sender_character_id NOT IN (
SELECT id FROM falukant_data.character SELECT id FROM falukant_data.character
) OR recipient_character_id NOT IN ( ) OR recipient_character_id NOT IN (
SELECT id FROM falukant_data.character SELECT id FROM falukant_data.character
); );
`); `, 30000, 'promotional_gift cleanup');
const deletedCount5 = result5[1] || 0; const deletedCount5 = result5[1] || 0;
if (deletedCount5 > 0) { if (deletedCount5 > 0) {
console.log(`${deletedCount5} verwaiste promotional_gift Einträge entfernt`); console.log(`${deletedCount5} verwaiste promotional_gift Einträge entfernt`);
} }
// Cleanup user_house mit ungültigen house_type_id oder user_id // Cleanup user_house mit ungültigen house_type_id oder user_id
const result6 = await sequelize.query(` console.log(" → Prüfe user_house...");
const result6 = await queryWithTimeout(`
DELETE FROM falukant_data.user_house DELETE FROM falukant_data.user_house
WHERE house_type_id NOT IN ( WHERE house_type_id NOT IN (
SELECT id FROM falukant_type.house SELECT id FROM falukant_type.house
) OR user_id NOT IN ( ) OR user_id NOT IN (
SELECT id FROM falukant_data.falukant_user SELECT id FROM falukant_data.falukant_user
); );
`); `, 30000, 'user_house cleanup');
const deletedCount6 = result6[1] || 0; const deletedCount6 = result6[1] || 0;
if (deletedCount6 > 0) { if (deletedCount6 > 0) {
console.log(`${deletedCount6} verwaiste user_house Einträge entfernt`); 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 // Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id
const result7 = await sequelize.query(` console.log(" → Prüfe child_relation...");
const result7 = await queryWithTimeout(`
DELETE FROM falukant_data.child_relation DELETE FROM falukant_data.child_relation
WHERE father_character_id NOT IN ( WHERE father_character_id NOT IN (
SELECT id FROM falukant_data.character SELECT id FROM falukant_data.character
@@ -490,14 +939,15 @@ const syncDatabaseForDeployment = async () => {
) OR child_character_id NOT IN ( ) OR child_character_id NOT IN (
SELECT id FROM falukant_data.character SELECT id FROM falukant_data.character
); );
`); `, 30000, 'child_relation cleanup');
const deletedCount7 = result7[1] || 0; const deletedCount7 = result7[1] || 0;
if (deletedCount7 > 0) { if (deletedCount7 > 0) {
console.log(`${deletedCount7} verwaiste child_relation Einträge entfernt`); console.log(`${deletedCount7} verwaiste child_relation Einträge entfernt`);
} }
// Cleanup political_office mit ungültigen character_id, office_type_id oder region_id // Cleanup political_office mit ungültigen character_id, office_type_id oder region_id
const result8 = await sequelize.query(` console.log(" → Prüfe political_office...");
const result8 = await queryWithTimeout(`
DELETE FROM falukant_data.political_office DELETE FROM falukant_data.political_office
WHERE character_id NOT IN ( WHERE character_id NOT IN (
SELECT id FROM falukant_data.character SELECT id FROM falukant_data.character
@@ -506,33 +956,74 @@ const syncDatabaseForDeployment = async () => {
) OR region_id NOT IN ( ) OR region_id NOT IN (
SELECT id FROM falukant_data.region SELECT id FROM falukant_data.region
); );
`); `, 30000, 'political_office cleanup');
const deletedCount8 = result8[1] || 0; const deletedCount8 = result8[1] || 0;
if (deletedCount8 > 0) { if (deletedCount8 > 0) {
console.log(`${deletedCount8} verwaiste political_office Einträge entfernt`); console.log(`${deletedCount8} verwaiste political_office Einträge entfernt`);
} }
// Cleanup church_office mit ungültigen character_id, office_type_id oder region_id (nur wenn Tabelle existiert)
if (await tableExists('falukant_data', 'church_office')) {
console.log(" → Prüfe church_office...");
const result11 = await queryWithTimeout(`
DELETE FROM falukant_data.church_office
WHERE character_id NOT IN (
SELECT id FROM falukant_data.character
) OR office_type_id NOT IN (
SELECT id FROM falukant_type.church_office_type
) OR region_id NOT IN (
SELECT id FROM falukant_data.region
);
`, 30000, 'church_office cleanup');
const deletedCount11 = result11[1] || 0;
if (deletedCount11 > 0) {
console.log(`${deletedCount11} verwaiste church_office Einträge entfernt`);
}
}
// Cleanup church_application mit ungültigen character_id, office_type_id, region_id oder supervisor_id (nur wenn Tabelle existiert)
if (await tableExists('falukant_data', 'church_application')) {
console.log(" → Prüfe church_application...");
const result12 = await queryWithTimeout(`
DELETE FROM falukant_data.church_application
WHERE character_id NOT IN (
SELECT id FROM falukant_data.character
) OR office_type_id NOT IN (
SELECT id FROM falukant_type.church_office_type
) OR region_id NOT IN (
SELECT id FROM falukant_data.region
) OR (supervisor_id IS NOT NULL AND supervisor_id NOT IN (
SELECT id FROM falukant_data.character
));
`, 30000, 'church_application cleanup');
const deletedCount12 = result12[1] || 0;
if (deletedCount12 > 0) {
console.log(`${deletedCount12} verwaiste church_application Einträge entfernt`);
}
}
// Cleanup vehicle.condition: Legacy-Nulls + Range clamp (UI zeigt sonst "Unbekannt") // Cleanup vehicle.condition: Legacy-Nulls + Range clamp (UI zeigt sonst "Unbekannt")
const result9 = await sequelize.query(` console.log(" → Prüfe vehicle.condition...");
const result9 = await queryWithTimeout(`
UPDATE falukant_data.vehicle UPDATE falukant_data.vehicle
SET condition = 100 SET condition = 100
WHERE condition IS NULL; WHERE condition IS NULL;
`); `, 30000, 'vehicle condition NULL update');
const updatedNullConditions = result9[1] || 0; const updatedNullConditions = result9[1] || 0;
if (updatedNullConditions > 0) { if (updatedNullConditions > 0) {
console.log(`${updatedNullConditions} vehicle.condition NULL → 100 gesetzt`); console.log(`${updatedNullConditions} vehicle.condition NULL → 100 gesetzt`);
} }
const result10 = await sequelize.query(` const result10 = await queryWithTimeout(`
UPDATE falukant_data.vehicle UPDATE falukant_data.vehicle
SET condition = GREATEST(0, LEAST(100, condition)) SET condition = GREATEST(0, LEAST(100, condition))
WHERE condition < 0 OR condition > 100; WHERE condition < 0 OR condition > 100;
`); `, 30000, 'vehicle condition clamp');
const clampedConditions = result10[1] || 0; const clampedConditions = result10[1] || 0;
if (clampedConditions > 0) { if (clampedConditions > 0) {
console.log(`${clampedConditions} vehicle.condition Werte auf 0..100 geklemmt`); console.log(`${clampedConditions} vehicle.condition Werte auf 0..100 geklemmt`);
} }
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0 && updatedNullConditions === 0 && clampedConditions === 0) { if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0 && deletedCount11 === 0 && deletedCount12 === 0 && updatedNullConditions === 0 && clampedConditions === 0) {
console.log("✅ Keine verwaisten Einträge gefunden"); console.log("✅ Keine verwaisten Einträge gefunden");
} }
} catch (e) { } catch (e) {
@@ -580,6 +1071,9 @@ const syncDatabaseForDeployment = async () => {
console.log("Initializing Taxi..."); console.log("Initializing Taxi...");
await initializeTaxi(); await initializeTaxi();
console.log("Initializing widget types...");
await initializeWidgetTypes();
console.log('Database synchronization for deployment complete.'); console.log('Database synchronization for deployment complete.');
} catch (error) { } catch (error) {
console.error('Unable to synchronize the database for deployment:', error); console.error('Unable to synchronize the database for deployment:', error);

113
backend/vacuum-database.js Executable file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env node
/**
* Script zum Ausführen von VACUUM auf Tabellen mit hohem Bloat
*
* Führt VACUUM ANALYZE auf Tabellen aus, die:
* - Hohen Bloat-Anteil haben (> 20% tote Zeilen)
* - Seit mehr als 7 Tagen nicht gevacuumt wurden
* - Viele tote Zeilen haben (> 1000)
*/
import './config/loadEnv.js';
import { sequelize } from './utils/sequelize.js';
async function main() {
try {
console.log('🧹 Datenbank-Vacuum\n');
console.log('='.repeat(60) + '\n');
// Finde Tabellen, die Vacuum benötigen
const [tablesToVacuum] = await sequelize.query(`
SELECT
schemaname || '.' || relname as table_name,
n_dead_tup,
n_live_tup,
CASE
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
ELSE 0
END as dead_percent,
last_vacuum,
last_autovacuum,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as table_size
FROM pg_stat_user_tables
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
AND (
(n_dead_tup > 1000 AND (n_live_tup = 0 OR (n_dead_tup::numeric / NULLIF(n_live_tup, 0)) > 0.2))
OR (last_vacuum IS NULL AND last_autovacuum IS NULL AND n_dead_tup > 0)
OR (
(last_vacuum < now() - interval '7 days' OR last_vacuum IS NULL)
AND (last_autovacuum < now() - interval '7 days' OR last_autovacuum IS NULL)
AND n_dead_tup > 100
)
)
ORDER BY n_dead_tup DESC;
`);
if (tablesToVacuum.length === 0) {
console.log('✅ Keine Tabellen benötigen Vacuum.\n');
await sequelize.close();
process.exit(0);
}
console.log(`📋 Gefunden: ${tablesToVacuum.length} Tabellen benötigen Vacuum\n`);
// Zeige Tabellen
console.log('Tabellen, die gevacuumt werden:');
tablesToVacuum.forEach((t, i) => {
console.log(` ${i + 1}. ${t.table_name}`);
console.log(` Größe: ${t.table_size}, Tote Zeilen: ${parseInt(t.n_dead_tup).toLocaleString()} (${t.dead_percent}%)`);
});
console.log('');
// Frage Bestätigung
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise((resolve) => {
rl.question('Möchtest du VACUUM ANALYZE auf diesen Tabellen ausführen? (j/n): ', resolve);
});
rl.close();
if (answer.toLowerCase() !== 'j' && answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'ja' && answer.toLowerCase() !== 'yes') {
console.log('❌ Abgebrochen.\n');
await sequelize.close();
process.exit(0);
}
console.log('\n🧹 Starte Vacuum...\n');
// Führe VACUUM ANALYZE aus
for (let i = 0; i < tablesToVacuum.length; i++) {
const table = tablesToVacuum[i];
const startTime = Date.now();
try {
console.log(`[${i + 1}/${tablesToVacuum.length}] Vacuuming ${table.table_name}...`);
await sequelize.query(`VACUUM ANALYZE ${table.table_name};`);
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(` ✅ Fertig in ${duration}s\n`);
} catch (error) {
console.error(` ❌ Fehler: ${error.message}\n`);
}
}
console.log('='.repeat(60));
console.log('✅ Vacuum abgeschlossen\n');
await sequelize.close();
process.exit(0);
} catch (error) {
console.error('❌ Fehler:', error.message);
console.error(error.stack);
process.exit(1);
}
}
main();

78
build-local.sh Executable file
View File

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

33
check-lesson-exercises.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Script zum Prüfen, ob Übungen für eine Lektion vorhanden sind
# Verwendung: ./check-lesson-exercises.sh [lesson_id]
LESSON_ID="${1:-1}"
echo "🔍 Prüfe Übungen für Lektion ID: $LESSON_ID"
echo ""
psql -U yourpart -d yp3 << EOF
-- Prüfe Lektion
SELECT
l.id,
l.title,
l.course_id,
COUNT(e.id) as exercise_count
FROM community.vocab_course_lesson l
LEFT JOIN community.vocab_grammar_exercise e ON e.lesson_id = l.id
WHERE l.id = $LESSON_ID
GROUP BY l.id, l.title, l.course_id;
-- Zeige alle Übungen für diese Lektion
SELECT
e.id,
e.exercise_number,
e.title,
e.exercise_type_id,
et.name as exercise_type_name
FROM community.vocab_grammar_exercise e
LEFT JOIN community.vocab_grammar_exercise_type et ON et.id = e.exercise_type_id
WHERE e.lesson_id = $LESSON_ID
ORDER BY e.exercise_number;
EOF

38
check-vocab-schema.sql Normal file
View File

@@ -0,0 +1,38 @@
-- ============================================
-- Prüfe ob alle notwendigen Spalten vorhanden sind
-- ============================================
-- Führe diese Queries auf dem Server aus, um zu prüfen, ob alles vorhanden ist
-- Prüfe native_language_id in vocab_course
SELECT
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'community'
AND table_name = 'vocab_course'
AND column_name = 'native_language_id';
-- Prüfe cultural_notes in vocab_course_lesson
SELECT
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'community'
AND table_name = 'vocab_course_lesson'
AND column_name = 'cultural_notes';
-- Prüfe ob vocab_grammar_exercise Tabelle existiert
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'community'
AND table_name = 'vocab_grammar_exercise'
);
-- Prüfe ob vocab_grammar_exercise_type Tabelle existiert
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'community'
AND table_name = 'vocab_grammar_exercise_type'
);

53
check-websocket-services.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
echo "=== WebSocket-Services Diagnose ==="
echo ""
echo "1. Prüfe Backend-Service-Status:"
sudo systemctl status yourpart-backend --no-pager -l | head -20
echo ""
echo "2. Prüfe Daemon-Service-Status:"
sudo systemctl status yourpart-daemon --no-pager -l | head -20
echo ""
echo "3. Prüfe Backend-Logs (letzte 30 Zeilen):"
sudo journalctl -u yourpart-backend -n 30 --no-pager | grep -E "(Socket|HTTPS|TLS|Port|4443|2020)" || echo "Keine relevanten Logs gefunden"
echo ""
echo "4. Prüfe Daemon-Logs (letzte 30 Zeilen):"
sudo journalctl -u yourpart-daemon -n 30 --no-pager | grep -E "(Daemon|WSS|TLS|Port|4551|connection)" || echo "Keine relevanten Logs gefunden"
echo ""
echo "5. Prüfe, welche Ports lauschen:"
echo " Port 2020 (API):"
sudo netstat -tlnp 2>/dev/null | grep ":2020 " || echo " ❌ Port 2020 lauscht nicht"
echo " Port 4443 (Socket.io):"
sudo netstat -tlnp 2>/dev/null | grep ":4443 " || echo " ❌ Port 4443 lauscht nicht"
echo " Port 4551 (Daemon):"
sudo netstat -tlnp 2>/dev/null | grep ":4551 " || echo " ❌ Port 4551 lauscht nicht"
echo ""
echo "6. Prüfe Firewall-Regeln:"
sudo ufw status | grep -E "(4443|4551)" || echo " Keine Firewall-Regeln für diese Ports gefunden"
echo ""
echo "7. Prüfe Backend-Umgebungsvariablen:"
sudo systemctl show yourpart-backend --property=Environment --no-pager | grep -E "(SOCKET_IO|TLS)" || echo " Keine Socket.io TLS-Variablen gefunden"
echo ""
echo "8. Prüfe Daemon-Umgebungsvariablen:"
sudo systemctl show yourpart-daemon --property=Environment --no-pager | grep -E "(DAEMON|TLS)" || echo " Keine Daemon TLS-Variablen gefunden"
echo ""
echo "9. Teste TLS-Zertifikate:"
if [ -f "/etc/letsencrypt/live/www.your-part.de/privkey.pem" ]; then
echo " ✅ Privkey gefunden"
else
echo " ❌ Privkey nicht gefunden"
fi
if [ -f "/etc/letsencrypt/live/www.your-part.de/fullchain.pem" ]; then
echo " ✅ Fullchain gefunden"
else
echo " ❌ Fullchain nicht gefunden"
fi

175
cmake/install-config.cmake Normal file
View File

@@ -0,0 +1,175 @@
# CMake-Skript für intelligente Konfigurationsdatei-Installation
# Fügt nur fehlende Keys hinzu, ohne bestehende Konfiguration zu überschreiben
# Pfade setzen
set(CONFIG_FILE "/etc/yourpart/daemon.conf")
set(TEMPLATE_FILE "/etc/yourpart/daemon.conf.example")
# Prüfe ob Template existiert (wurde von CMake installiert)
if(NOT EXISTS "${TEMPLATE_FILE}")
# Fallback 1: Versuche Template im Source-Verzeichnis zu finden
# CMAKE_CURRENT_LIST_DIR zeigt auf cmake/ während der Installation
get_filename_component(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
set(TEMPLATE_FILE_FALLBACK "${PROJECT_ROOT}/daemon.conf")
# Fallback 2: Versuche über CMAKE_SOURCE_DIR (falls verfügbar)
if(DEFINED CMAKE_SOURCE_DIR AND EXISTS "${CMAKE_SOURCE_DIR}/daemon.conf")
set(TEMPLATE_FILE "${CMAKE_SOURCE_DIR}/daemon.conf")
message(STATUS "Verwende Template aus CMAKE_SOURCE_DIR: ${TEMPLATE_FILE}")
elseif(EXISTS "${TEMPLATE_FILE_FALLBACK}")
set(TEMPLATE_FILE "${TEMPLATE_FILE_FALLBACK}")
message(STATUS "Verwende Template aus Source-Verzeichnis: ${TEMPLATE_FILE}")
else()
message(FATAL_ERROR "Template-Datei nicht gefunden!")
message(FATAL_ERROR " Gesucht in: ${TEMPLATE_FILE}")
message(FATAL_ERROR " Fallback 1: ${TEMPLATE_FILE_FALLBACK}")
if(DEFINED CMAKE_SOURCE_DIR)
message(FATAL_ERROR " Fallback 2: ${CMAKE_SOURCE_DIR}/daemon.conf")
endif()
endif()
else()
message(STATUS "Verwende installierte Template-Datei: ${TEMPLATE_FILE}")
endif()
# Prüfe ob Ziel-Verzeichnis existiert
if(NOT EXISTS "/etc/yourpart")
message(STATUS "Erstelle Verzeichnis /etc/yourpart...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E make_directory "/etc/yourpart"
RESULT_VARIABLE MKDIR_RESULT
)
if(NOT MKDIR_RESULT EQUAL 0)
message(FATAL_ERROR "Konnte Verzeichnis /etc/yourpart nicht erstellen")
endif()
endif()
# Prüfe ob Config-Datei existiert
if(NOT EXISTS "${CONFIG_FILE}")
message(STATUS "Konfigurationsdatei existiert nicht, erstelle neue...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E copy "${TEMPLATE_FILE}" "${CONFIG_FILE}"
RESULT_VARIABLE COPY_RESULT
)
if(NOT COPY_RESULT EQUAL 0)
message(FATAL_ERROR "Konnte Konfigurationsdatei nicht erstellen: ${CONFIG_FILE}")
endif()
message(STATUS "Neue Konfigurationsdatei erstellt: ${CONFIG_FILE}")
else()
message(STATUS "Konfigurationsdatei existiert bereits, prüfe auf fehlende Keys...")
# Verwende ein Python-Skript für intelligentes Merging
# (CMake hat keine gute Unterstützung für komplexe String-Manipulation)
# Erstelle temporäres Python-Skript im Build-Verzeichnis
set(MERGE_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/merge-config.py")
# Erstelle Python-Skript
file(WRITE "${MERGE_SCRIPT}"
"#!/usr/bin/env python3
import sys
import re
import os
def merge_config(template_file, config_file):
\"\"\"Fügt fehlende Keys aus Template zur Config hinzu, ohne bestehende zu überschreiben\"\"\"
# Lese bestehende Config
existing_keys = {}
existing_lines = []
if os.path.exists(config_file):
with open(config_file, 'r') as f:
for line in f:
existing_lines.append(line.rstrip())
# Extrahiere Key=Value Paare
match = re.match(r'^\\s*([^#=]+?)\\s*=\\s*(.+?)\\s*$', line)
if match:
key = match.group(1).strip()
value = match.group(2).strip()
existing_keys[key] = value
# Lese Template
new_keys = {}
if not os.path.exists(template_file):
print(f'Fehler: Template-Datei {template_file} nicht gefunden!', file=sys.stderr)
return False
with open(template_file, 'r') as f:
for line in f:
# Extrahiere Key=Value Paare
match = re.match(r'^\\s*([^#=]+?)\\s*=\\s*(.+?)\\s*$', line)
if match:
key = match.group(1).strip()
value = match.group(2).strip()
new_keys[key] = value
# Füge fehlende Keys hinzu
added_count = 0
for key, value in new_keys.items():
if key not in existing_keys:
existing_lines.append(f'{key}={value}')
print(f'Füge fehlenden Key hinzu: {key}')
added_count += 1
# Schreibe aktualisierte Config
if added_count > 0:
with open(config_file, 'w') as f:
for line in existing_lines:
f.write(line + '\\n')
print(f'{added_count} neue Keys hinzugefügt')
else:
print('Keine neuen Keys hinzugefügt - Konfiguration ist aktuell')
return True
if __name__ == '__main__':
if len(sys.argv) != 3:
print('Verwendung: merge-config.py <template> <config>', file=sys.stderr)
sys.exit(1)
template_file = sys.argv[1]
config_file = sys.argv[2]
if not merge_config(template_file, config_file):
sys.exit(1)
")
# Setze Ausführungsrechte
file(CHMOD "${MERGE_SCRIPT}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
# Führe Merge-Skript aus
execute_process(
COMMAND python3 "${MERGE_SCRIPT}" "${TEMPLATE_FILE}" "${CONFIG_FILE}"
RESULT_VARIABLE MERGE_RESULT
OUTPUT_VARIABLE MERGE_OUTPUT
ERROR_VARIABLE MERGE_ERROR
)
if(NOT MERGE_RESULT EQUAL 0)
message(WARNING "Fehler beim Mergen der Config: ${MERGE_ERROR}")
else()
message(STATUS "${MERGE_OUTPUT}")
endif()
endif()
# Setze korrekte Berechtigungen (Fehler werden ignoriert, da Berechtigungen optional sind)
execute_process(
COMMAND chown yourpart:yourpart "${CONFIG_FILE}"
RESULT_VARIABLE CHOWN_RESULT
ERROR_QUIET
)
if(NOT CHOWN_RESULT EQUAL 0)
message(WARNING "Konnte Besitzer von ${CONFIG_FILE} nicht ändern (möglicherweise kein Root oder User existiert nicht)")
endif()
execute_process(
COMMAND chmod 600 "${CONFIG_FILE}"
RESULT_VARIABLE CHMOD_RESULT
ERROR_QUIET
)
if(NOT CHMOD_RESULT EQUAL 0)
message(WARNING "Konnte Berechtigungen von ${CONFIG_FILE} nicht ändern")
endif()
message(STATUS "Konfigurationsdatei-Verwaltung abgeschlossen: ${CONFIG_FILE}")

10
daemon.conf Normal file
View File

@@ -0,0 +1,10 @@
DB_HOST=localhost
DB_PORT=5432
DB_NAME=yp3
DB_USER=yourpart
DB_PASSWORD=hitomisan
THREAD_COUNT=4
WEBSOCKET_PORT=4551
WEBSOCKET_SSL_ENABLED=false
WEBSOCKET_SSL_CERT_PATH=/home/torsten/Programs/yourpart-daemon/ssl-certs/server.crt
WEBSOCKET_SSL_KEY_PATH=/home/torsten/Programs/yourpart-daemon/ssl-certs/server.key

5
daemon.log Normal file
View File

@@ -0,0 +1,5 @@
WebSocket Server starting on port 4551 (no SSL)
[2025/09/29 08:50:10:6854] N: lws_create_context: LWS: 4.3.5-unknown, NET CLI SRV H1 H2 WS ConMon IPv6-absent
[2025/09/29 08:50:10:6874] N: __lws_lc_tag: ++ [wsi|0|pipe] (1)
[2025/09/29 08:50:10:6874] N: __lws_lc_tag: ++ [vh|0|netlink] (1)
WebSocket-Server erfolgreich gestartet auf Port 4551

78
debug-lesson-exercises.js Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* Debug-Script zum Prüfen, ob Übungen für eine Lektion vorhanden sind
* Verwendung: node debug-lesson-exercises.js [lessonId]
*/
import { sequelize } from './backend/utils/sequelize.js';
import VocabCourseLesson from './backend/models/community/vocab_course_lesson.js';
import VocabGrammarExercise from './backend/models/community/vocab_grammar_exercise.js';
const lessonId = process.argv[2] ? parseInt(process.argv[2]) : 1;
async function debugLesson() {
try {
await sequelize.authenticate();
console.log('✅ Datenbankverbindung erfolgreich\n');
// Lade Lektion mit Übungen (wie im Backend)
const lesson = await VocabCourseLesson.findByPk(lessonId, {
include: [
{
model: require('./backend/models/community/vocab_course.js').default,
as: 'course'
},
{
model: VocabGrammarExercise,
as: 'grammarExercises',
include: [
{
model: require('./backend/models/community/vocab_grammar_exercise_type.js').default,
as: 'exerciseType'
}
],
required: false,
separate: true,
order: [['exerciseNumber', 'ASC']]
}
]
});
if (!lesson) {
console.log(`❌ Lektion ${lessonId} nicht gefunden`);
return;
}
console.log(`📚 Lektion: ${lesson.title} (ID: ${lesson.id})`);
console.log(` Kurs: ${lesson.course?.title || 'N/A'}`);
console.log(` Übungen (via Include): ${lesson.grammarExercises ? lesson.grammarExercises.length : 0}\n`);
// Prüfe direkt in der Datenbank
const directExercises = await VocabGrammarExercise.findAll({
where: { lessonId: lesson.id },
order: [['exerciseNumber', 'ASC']]
});
console.log(`📊 Direkte Abfrage: ${directExercises.length} Übung(en) gefunden`);
directExercises.forEach((ex, idx) => {
console.log(` ${idx + 1}. ${ex.title} (ID: ${ex.id}, Typ: ${ex.exerciseTypeId})`);
});
// Plain object
const plain = lesson.get({ plain: true });
console.log(`\n📦 Plain Object:`);
console.log(` grammarExercises: ${plain.grammarExercises ? plain.grammarExercises.length : 'undefined'}`);
if (plain.grammarExercises && plain.grammarExercises.length > 0) {
plain.grammarExercises.forEach((ex, idx) => {
console.log(` ${idx + 1}. ${ex.title} (ID: ${ex.id})`);
});
}
} catch (error) {
console.error('❌ Fehler:', error);
} finally {
await sequelize.close();
}
}
debugLesson();

View File

@@ -34,6 +34,16 @@ echo "VITE_API_BASE_URL=$VITE_API_BASE_URL"
echo "VITE_DAEMON_SOCKET=$VITE_DAEMON_SOCKET" echo "VITE_DAEMON_SOCKET=$VITE_DAEMON_SOCKET"
echo "VITE_CHAT_WS_URL=$VITE_CHAT_WS_URL" echo "VITE_CHAT_WS_URL=$VITE_CHAT_WS_URL"
# 5a. Dependencies installieren
echo "Installiere Dependencies..."
npm install
if [ $? -ne 0 ]; then
echo "❌ npm install fehlgeschlagen!"
exit 1
fi
# 5b. Frontend neu bauen
npm run build npm run build
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then

203
deploy-server.sh Executable file
View File

@@ -0,0 +1,203 @@
#!/bin/bash
# YourPart Daemon Server-Side Deployment Script
# Führen Sie dieses Script auf dem Server aus, nachdem Sie den Code hochgeladen haben
set -euo pipefail
# Farben für Output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Konfiguration
DAEMON_USER="yourpart"
PROJECT_NAME="yourpart-daemon"
REMOTE_DIR="/opt/yourpart"
SERVICE_NAME="yourpart-daemon"
BUILD_DIR="build"
# Funktionen
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Prüfe ob wir im richtigen Verzeichnis sind
if [ ! -f "CMakeLists.txt" ] || [ ! -f "daemon.conf" ]; then
log_error "Bitte führen Sie dieses Script aus dem Projektverzeichnis aus!"
log_info "Stellen Sie sicher, dass CMakeLists.txt und daemon.conf vorhanden sind."
exit 1
fi
log_info "Starte Server-Side Deployment für YourPart Daemon..."
# 1. Prüfe Dependencies
# Prüfe ob wir root-Rechte haben für bestimmte Operationen
check_sudo() {
if ! sudo -n true 2>/dev/null; then
log_info "Einige Operationen benötigen sudo-Rechte..."
fi
}
log_info "Prüfe Dependencies..."
if ! command -v cmake &> /dev/null; then
log_error "CMake nicht gefunden. Führen Sie zuerst install-dependencies-ubuntu22.sh aus!"
exit 1
fi
if ! command -v gcc-15 &> /dev/null && ! command -v gcc &> /dev/null; then
log_error "GCC nicht gefunden. Führen Sie zuerst install-dependencies-ubuntu22.sh aus!"
exit 1
fi
# 2. Baue Projekt
log_info "Baue Projekt auf dem Server..."
if [ ! -d "$BUILD_DIR" ]; then
mkdir "$BUILD_DIR"
fi
cd "$BUILD_DIR"
# Konfiguriere CMake
log_info "Konfiguriere CMake..."
if command -v gcc-15 &> /dev/null; then
log_info "Verwende GCC 15 für C++23"
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=23 -DCMAKE_C_COMPILER=gcc-15 -DCMAKE_CXX_COMPILER=g++-15
elif command -v gcc-13 &> /dev/null; then
log_info "Verwende GCC 13 für C++23"
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=23 -DCMAKE_C_COMPILER=gcc-13 -DCMAKE_CXX_COMPILER=g++-13
else
log_info "Verwende Standard-GCC 11 mit C++20"
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=20
fi
# Kompiliere
log_info "Kompiliere Projekt..."
make -j$(nproc)
cd ..
log_success "Build abgeschlossen"
# 3. Erstelle Benutzer falls nicht vorhanden
log_info "Prüfe Benutzer $DAEMON_USER..."
if ! id "$DAEMON_USER" &>/dev/null; then
log_info "Erstelle Benutzer $DAEMON_USER..."
sudo useradd --system --shell /bin/false --home-dir "$REMOTE_DIR" --create-home "$DAEMON_USER"
log_success "Benutzer $DAEMON_USER erstellt"
else
log_info "Benutzer $DAEMON_USER existiert bereits"
fi
# 4. Erstelle Verzeichnisse
log_info "Erstelle Verzeichnisse..."
mkdir -p "$REMOTE_DIR"/{logs,config}
sudo mkdir -p /etc/yourpart
sudo mkdir -p /var/log/yourpart
# 5. Stoppe Service falls läuft
log_info "Stoppe Service falls läuft..."
if sudo systemctl is-active --quiet "$SERVICE_NAME"; then
log_info "Stoppe laufenden Service..."
sudo systemctl stop "$SERVICE_NAME"
sleep 2
fi
# 6. Kopiere Dateien
log_info "Kopiere Dateien..."
sudo cp "$BUILD_DIR/yourpart-daemon" /usr/local/bin/
# Intelligente Konfigurationsdatei-Verwaltung
log_info "Verwalte Konfigurationsdatei..."
if [ ! -f "/etc/yourpart/daemon.conf" ]; then
log_info "Konfigurationsdatei existiert nicht, kopiere neue..."
sudo cp daemon.conf /etc/yourpart/
sudo chown yourpart:yourpart /etc/yourpart/daemon.conf
else
log_info "Konfigurationsdatei existiert bereits, prüfe auf fehlende Keys..."
# Erstelle temporäre Datei mit neuen Keys
temp_conf="/tmp/daemon.conf.new"
cp daemon.conf "$temp_conf"
# Füge fehlende Keys hinzu
while IFS='=' read -r key value; do
# Überspringe Kommentare und leere Zeilen
if [[ "$key" =~ ^[[:space:]]*# ]] || [[ -z "$key" ]]; then
continue
fi
# Entferne Leerzeichen am Anfang
key=$(echo "$key" | sed 's/^[[:space:]]*//')
# Prüfe ob Key bereits existiert
if ! grep -q "^[[:space:]]*$key[[:space:]]*=" /etc/yourpart/daemon.conf; then
log_info "Füge fehlenden Key hinzu: $key"
echo "$key=$value" | sudo tee -a /etc/yourpart/daemon.conf > /dev/null
fi
done < "$temp_conf"
rm -f "$temp_conf"
fi
sudo cp yourpart-daemon.service /etc/systemd/system/
# 7. Setze Berechtigungen
log_info "Setze Berechtigungen..."
sudo chmod +x /usr/local/bin/yourpart-daemon
sudo chown -R "$DAEMON_USER:$DAEMON_USER" "$REMOTE_DIR"
sudo chown -R "$DAEMON_USER:$DAEMON_USER" /var/log/yourpart
sudo chown yourpart:yourpart /etc/yourpart/daemon.conf
sudo chmod 600 /etc/yourpart/daemon.conf
# 8. Lade systemd neu
log_info "Lade systemd Konfiguration neu..."
sudo systemctl daemon-reload
# 9. Aktiviere Service
log_info "Aktiviere Service..."
sudo systemctl enable "$SERVICE_NAME"
# 10. Starte Service
log_info "Starte Service..."
sudo systemctl start "$SERVICE_NAME" &
sleep 3
# 11. Prüfe Status
log_info "Prüfe Service-Status..."
sleep 2
if sudo systemctl is-active --quiet "$SERVICE_NAME"; then
log_success "Service läuft erfolgreich!"
sudo systemctl status "$SERVICE_NAME" --no-pager
else
log_error "Service konnte nicht gestartet werden!"
log_info "Logs anzeigen mit: sudo journalctl -u $SERVICE_NAME -f"
exit 1
fi
# 11. Zeige nützliche Befehle
log_success "Deployment erfolgreich abgeschlossen!"
log_info ""
log_info "Nützliche Befehle:"
log_info "- Service-Status: sudo systemctl status $SERVICE_NAME"
log_info "- Service stoppen: sudo systemctl stop $SERVICE_NAME"
log_info "- Service starten: sudo systemctl start $SERVICE_NAME"
log_info "- Service neustarten: sudo systemctl restart $SERVICE_NAME"
log_info "- Logs anzeigen: sudo journalctl -u $SERVICE_NAME -f"
log_info "- Logs der letzten 100 Zeilen: sudo journalctl -u $SERVICE_NAME -n 100"
log_info ""
log_info "Konfigurationsdatei: /etc/yourpart/daemon.conf"
log_info "Log-Verzeichnis: /var/log/yourpart/"
log_info "Service-Datei: /etc/systemd/system/$SERVICE_NAME.service"

203
deploy.sh
View File

@@ -1,36 +1,183 @@
# YourPart Daemon Deployment Script für Ubuntu 22
# Verwendung: ./deploy.sh [server_ip] [ssh_user]
set -euo pipefail
# Farben für Output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Konfiguration
SERVER_IP="${1:-your-part.de}"
SSH_USER="${2:-root}"
DAEMON_USER="yourpart"
PROJECT_NAME="yourpart-daemon"
REMOTE_DIR="/opt/yourpart"
SERVICE_NAME="yourpart-daemon"
# Funktionen
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Prüfe ob wir im richtigen Verzeichnis sind
if [ ! -f "CMakeLists.txt" ] || [ ! -f "daemon.conf" ]; then
log_error "Bitte führen Sie dieses Script aus dem Projektverzeichnis aus!"
exit 1
fi
log_info "Starte Deployment für YourPart Daemon..."
log_info "Server: $SERVER_IP"
log_info "SSH User: $SSH_USER"
# 1. Lokales Build
log_info "Baue Projekt lokal..."
if [ ! -d "build" ]; then
mkdir build
fi
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=23
make -j$(nproc)
cd ..
log_success "Lokaler Build abgeschlossen"
# 2. Erstelle Deployment-Paket
log_info "Erstelle Deployment-Paket..."
DEPLOY_DIR="deploy_package"
rm -rf "$DEPLOY_DIR"
mkdir -p "$DEPLOY_DIR"
# Kopiere Binärdatei
cp build/yourpart-daemon "$DEPLOY_DIR/"
# Kopiere Konfigurationsdatei
cp daemon.conf "$DEPLOY_DIR/"
# Kopiere Service-Datei
cp yourpart-daemon.service "$DEPLOY_DIR/"
# Erstelle Installations-Script
cat > "$DEPLOY_DIR/install.sh" << 'EOF'
#!/bin/bash #!/bin/bash
set -euo pipefail
echo "=== YourPart Deployment Script ===" # Farben
echo "" RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Prüfen ob wir im richtigen Verzeichnis sind log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
if [ ! -f "package.json" ]; then log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
echo "Error: Please run this script from the YourPart3 root directory" log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
exit 1 log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
DAEMON_USER="yourpart"
REMOTE_DIR="/opt/yourpart"
SERVICE_NAME="yourpart-daemon"
log_info "Installiere YourPart Daemon..."
# Erstelle Benutzer falls nicht vorhanden
if ! id "$DAEMON_USER" &>/dev/null; then
log_info "Erstelle Benutzer $DAEMON_USER..."
useradd --system --shell /bin/false --home-dir "$REMOTE_DIR" --create-home "$DAEMON_USER"
log_success "Benutzer $DAEMON_USER erstellt"
else
log_info "Benutzer $DAEMON_USER existiert bereits"
fi fi
# Prüfen ob sudo verfügbar ist # Erstelle Verzeichnisse
if ! command -v sudo &> /dev/null; then log_info "Erstelle Verzeichnisse..."
echo "Error: sudo is required but not installed" mkdir -p "$REMOTE_DIR"/{logs,config}
exit 1 mkdir -p /etc/yourpart
fi mkdir -p /var/log/yourpart
# Backend deployen # Kopiere Dateien
echo "" log_info "Kopiere Dateien..."
echo "=== Deploying Backend ===" cp yourpart-daemon /usr/local/bin/
./deploy-backend.sh cp daemon.conf /etc/yourpart/
cp yourpart-daemon.service /etc/systemd/system/
# Frontend bauen und deployen # Setze Berechtigungen
echo "" chmod +x /usr/local/bin/yourpart-daemon
echo "=== Building and Deploying Frontend ===" chown -R "$DAEMON_USER:$DAEMON_USER" "$REMOTE_DIR"
./deploy-frontend.sh chown -R "$DAEMON_USER:$DAEMON_USER" /var/log/yourpart
chmod 600 /etc/yourpart/daemon.conf
echo "" # Lade systemd neu
echo "=== Deployment Completed! ===" log_info "Lade systemd Konfiguration neu..."
echo "Your application should now be available at:" systemctl daemon-reload
echo " HTTP: http://your-part.de (redirects to HTTPS)"
echo " HTTPS: https://www.your-part.de" # Aktiviere Service
echo "" log_info "Aktiviere Service..."
echo "To check logs:" systemctl enable "$SERVICE_NAME"
echo " Backend: sudo journalctl -u yourpart.service -f"
echo " Apache: sudo tail -f /var/log/apache2/yourpart.*.log" log_success "Installation abgeschlossen!"
log_info "Verwenden Sie 'systemctl start $SERVICE_NAME' um den Service zu starten"
log_info "Verwenden Sie 'systemctl status $SERVICE_NAME' um den Status zu prüfen"
log_info "Verwenden Sie 'journalctl -u $SERVICE_NAME -f' um die Logs zu verfolgen"
EOF
chmod +x "$DEPLOY_DIR/install.sh"
# Erstelle Tarball
tar -czf "${PROJECT_NAME}_deploy.tar.gz" -C "$DEPLOY_DIR" .
log_success "Deployment-Paket erstellt: ${PROJECT_NAME}_deploy.tar.gz"
# 3. Upload zum Server
log_info "Lade Dateien zum Server hoch..."
scp "${PROJECT_NAME}_deploy.tar.gz" "$SSH_USER@$SERVER_IP:/tmp/"
# 4. Installation auf dem Server
log_info "Installiere auf dem Server..."
ssh "$SSH_USER@$SERVER_IP" << EOF
set -euo pipefail
# Entpacke Deployment-Paket
cd /tmp
tar -xzf "${PROJECT_NAME}_deploy.tar.gz"
# Führe Installation aus
./install.sh
# Starte Service
systemctl start $SERVICE_NAME
# Prüfe Status
systemctl status $SERVICE_NAME --no-pager
# Aufräumen
rm -f "${PROJECT_NAME}_deploy.tar.gz"
rm -rf /tmp/yourpart-daemon /tmp/daemon.conf /tmp/yourpart-daemon.service /tmp/install.sh
echo "Deployment erfolgreich abgeschlossen!"
EOF
# 5. Aufräumen
log_info "Räume lokale Dateien auf..."
rm -rf "$DEPLOY_DIR"
rm -f "${PROJECT_NAME}_deploy.tar.gz"
log_success "Deployment erfolgreich abgeschlossen!"
log_info "Der YourPart Daemon läuft jetzt auf $SERVER_IP"
log_info "Verwenden Sie 'ssh $SSH_USER@$SERVER_IP systemctl status $SERVICE_NAME' um den Status zu prüfen"

View File

@@ -0,0 +1,306 @@
# Bisaya (Cebuano) 4-Wochen-Kurs - Vollständige Struktur
## Kurs-Übersicht
**Titel:** Bisaya für Familien - Schnellstart in 4 Wochen
**Ziel:** In 4 Wochen alltagstauglich Bisaya sprechen können
**Zeitaufwand:** ~30-40 Minuten pro Tag
**Schwierigkeit:** Anfänger (Level 1)
## Lernziele & System
### Zeitvorgaben pro Lektionstyp:
- **Vokabeln:** 20-25 Minuten pro Tag
- **Konversation:** 15-20 Minuten pro Tag
- **Grammatik:** 20-25 Minuten pro Tag
- **Wiederholung:** 30 Minuten pro Tag
- **Tests:** 15-20 Minuten
### Zielpunktzahlen:
- **Vokabeln:** 85% zum Abschluss (sonst Wiederholung erforderlich)
- **Konversation:** 80% zum Abschluss
- **Grammatik:** 75% zum Abschluss
- **Tests:** 80% zum Abschluss
### Wiederholungslogik:
- Lektionen mit `requiresReview: true` müssen wiederholt werden, wenn das Ziel nicht erreicht wird
- Vokabel-Lektionen haben standardmäßig Wiederholung aktiviert
- Konversations-Lektionen können ohne Wiederholung abgeschlossen werden
## Woche 1: Grundlagen & Aussprache
### Tag 1 (35 Min)
1. **Begrüßungen & Höflichkeit** (15 Min, 80%, conversation)
- Kumusta? Wie geht's?
- Maayo Gut
- Salamat Danke
- Palihug Bitte
- Kulturell: Philippiner schätzen Höflichkeit sehr. Lächeln ist wichtig!
2. **Überlebenssätze - Teil 1** (20 Min, 85%, vocab, Wiederholung)
- Die 10 wichtigsten Sätze für den Alltag
- Wala ko kasabot Ich verstehe nicht
- Hinay-hinay lang Bitte langsam
- Asa ang …? Wo ist …?
### Tag 2 (35 Min)
3. **Familienwörter** (20 Min, 85%, vocab, Wiederholung)
- Mama / Nanay Mutter
- Papa / Tatay Vater
- Kuya älterer Bruder
- Ate ältere Schwester
- Lola / Lolo Oma / Opa
- Kulturell: Kuya und Ate werden auch für Nicht-Verwandte verwendet!
4. **Familien-Gespräche** (15 Min, 80%, conversation)
- Einfache Gespräche mit Familienmitgliedern
- Kumusta mo? Wie geht es dir?
### Tag 3 (35 Min)
5. **Gefühle & Zuneigung** (15 Min, 80%, conversation)
- Mingaw ko nimo Ich vermisse dich
- Nalipay ko nga makita ka Ich freue mich, dich zu sehen
- Ganahan ko nimo Ich mag dich
- Palangga taka Ich hab dich lieb ❤️
- Kulturell: Palangga taka ist wärmer als "I love you"!
6. **Überlebenssätze - Teil 2** (20 Min, 85%, vocab, Wiederholung)
- Weitere wichtige Alltagssätze
- Unsa ni? Was ist das?
- Oo / Dili Ja / Nein
### Tag 4 (35 Min)
7. **Essen & Fürsorge** (15 Min, 80%, conversation)
- Nikaon ka? Hast du schon gegessen?
- Kaon ta Lass uns essen
- Lami Lecker
- Kulturell: Essen = Liebe! "Nikaon na ka?" ist sehr fürsorglich.
8. **Essen & Trinken** (20 Min, 85%, vocab, Wiederholung)
- Wichtige Wörter rund ums Essen
### Tag 5 (45 Min)
9. **Woche 1 - Wiederholung** (30 Min, 80%, review)
- Wiederhole alle Inhalte der ersten Woche
10. **Woche 1 - Vokabeltest** (15 Min, 80%, vocab, Wiederholung)
- Teste dein Wissen aus Woche 1
## Woche 2: Alltag & Familie
### Tag 6 (35 Min)
11. **Alltagsgespräche - Teil 1** (15 Min, 80%, conversation)
- Kumusta ang imong adlaw? Wie war dein Tag?
- Unsa imong ginabuhat? Was machst du?
12. **Haus & Familie** (20 Min, 85%, vocab, Wiederholung)
- Balay Haus
- Kwarto Zimmer
- Kusina Küche
- Pamilya Familie
### Tag 7 (35 Min)
13. **Alltagsgespräche - Teil 2** (15 Min, 80%, conversation)
- Asa ka padulong? Wohin gehst du?
- Unsa imong plano? Was ist dein Plan?
14. **Ort & Richtung** (20 Min, 85%, vocab, Wiederholung)
- Asa Wo
- dinhi hier
- didto dort
- padulong gehen zu
### Tag 8 (45 Min)
15. **Zeitformen - Grundlagen** (25 Min, 75%, grammar, Wiederholung)
- Ni-kaon ko → Ich habe gegessen
- Mo-kaon ko → Ich werde essen
- Kulturell: Cebuano hat keine komplexen Zeiten wie Deutsch!
16. **Zeit & Datum** (20 Min, 85%, vocab, Wiederholung)
- Karon jetzt
- ugma morgen
- gahapon gestern
- karon adlaw heute
### Tag 9 (40 Min)
17. **Einkaufen & Preise** (15 Min, 80%, conversation)
- Tagpila ni? Wie viel kostet das?
- Pwede barato? Kann es billiger sein?
- Kulturell: Handeln ist in den Philippinen üblich!
18. **Zahlen & Preise** (25 Min, 85%, vocab, Wiederholung)
- Zahlen 1-100
- Preise und Mengen
### Tag 10 (45 Min)
19. **Woche 2 - Wiederholung** (30 Min, 80%, review)
- Wiederhole alle Inhalte der zweiten Woche
20. **Woche 2 - Vokabeltest** (15 Min, 80%, vocab, Wiederholung)
- Teste dein Wissen aus Woche 2
## Woche 3: Vertiefung
### Tag 11 (35 Min)
21. **Gefühle & Emotionen** (15 Min, 80%, conversation)
- Nalipay glücklich
- nasubo traurig
- nahadlok ängstlich
22. **Gefühle & Emotionen** (20 Min, 85%, vocab, Wiederholung)
- Wörter für verschiedene Gefühle
### Tag 12 (35 Min)
23. **Gesundheit & Wohlbefinden** (15 Min, 80%, conversation)
- Sakit Schmerz/Krankheit
- maayo gut
- tambal Medizin
- doktor Arzt
24. **Körper & Gesundheit** (20 Min, 85%, vocab, Wiederholung)
- Wörter rund um den Körper
### Tag 13 (35 Min)
25. **Höflichkeitsformen** (20 Min, 75%, grammar, Wiederholung)
- Palihug Bitte
- Pwede Kann ich?
- Tabang Hilfe
- Kulturell: Höflichkeit ist extrem wichtig!
26. **Bitten & Fragen** (15 Min, 80%, conversation)
- Wie man höflich fragt und bittet
### Tag 14 (35 Min)
27. **Kinder & Familie** (15 Min, 80%, conversation)
- Gespräche mit und über Kinder
- Kulturell: Kinder sind sehr wichtig!
28. **Kinder & Spiel** (20 Min, 85%, vocab, Wiederholung)
- Wörter für Kinder und Spielsachen
### Tag 15 (45 Min)
29. **Woche 3 - Wiederholung** (30 Min, 80%, review)
- Wiederhole alle Inhalte der dritten Woche
30. **Woche 3 - Vokabeltest** (15 Min, 80%, vocab, Wiederholung)
- Teste dein Wissen aus Woche 3
## Woche 4: Freies Sprechen
### Tag 16 (45 Min)
31. **Freies Gespräch - Thema 1** (20 Min, 75%, conversation)
- Übe freies Sprechen zu verschiedenen Themen
- Kulturell: Fehler sind okay! Philippiner schätzen das Bemühen.
32. **Wiederholung - Woche 1 & 2** (25 Min, 85%, vocab, Wiederholung)
- Wiederhole wichtige Vokabeln
### Tag 17 (45 Min)
33. **Freies Gespräch - Thema 2** (20 Min, 75%, conversation)
- Weitere Übung im freien Sprechen
34. **Wiederholung - Woche 3** (25 Min, 85%, vocab, Wiederholung)
- Wiederhole wichtige Vokabeln aus Woche 3
### Tag 18 (55 Min)
35. **Komplexere Gespräche** (25 Min, 75%, conversation)
- Längere Gespräche zu verschiedenen Themen
- Kulturell: Je mehr du sprichst, desto besser wirst du!
36. **Gesamtwiederholung** (30 Min, 80%, review)
- Wiederhole alle wichtigen Inhalte
### Tag 19 (45 Min)
37. **Praktische Übung** (25 Min, 75%, conversation)
- Simuliere echte Gesprächssituationen
38. **Abschlusstest - Vokabeln** (20 Min, 80%, vocab, Wiederholung)
- Finaler Vokabeltest über den gesamten Kurs
### Tag 20 (45 Min)
39. **Abschlussprüfung** (30 Min, 80%, review)
- Finale Prüfung über alle Kursinhalte
- Kulturell: Gratulation zum Abschluss!
40. **Kulturelle Tipps & Tricks** (15 Min, 0%, culture)
- Wichtige kulturelle Hinweise für den Alltag
- Kulturelles Verständnis ist genauso wichtig wie die Sprache!
## Kurs-Statistik
- **Gesamte Lektionen:** 40
- **Vokabel-Lektionen:** 15 (mit Wiederholung)
- **Konversations-Lektionen:** 15
- **Grammatik-Lektionen:** 2
- **Wiederholungs-Lektionen:** 6
- **Kultur-Lektionen:** 1
- **Test-Lektionen:** 1
- **Durchschnittliche Zeit pro Tag:** ~38 Minuten
- **Gesamtzeit:** ~15 Stunden über 4 Wochen
## Verwendung des Scripts
```bash
# 1. Erstelle zuerst die Sprache "Cebuano" oder "Bisaya" im System
# 2. Notiere dir die languageId und deine ownerHashedId
# 3. Führe das Script aus:
node backend/scripts/create-bisaya-course.js <languageId> <ownerHashedId>
# Beispiel:
node backend/scripts/create-bisaya-course.js 1 abc123def456
```
Das Script erstellt automatisch:
- Den Kurs mit allen 40 Lektionen
- Wochen- und Tagesstruktur
- Zeitvorgaben und Zielpunktzahlen
- Wiederholungslogik
- Kulturelle Notizen
## Nächste Schritte nach Kurs-Erstellung
1. **Vokabeln hinzufügen:**
- Erstelle Kapitel für jede Vokabel-Lektion
- Füge die entsprechenden Vokabeln hinzu
- Verknüpfe die Kapitel mit den Lektionen
2. **Grammatik-Übungen erstellen:**
- Erstelle Grammatik-Übungen für Lektion 15 und 25
- Nutze verschiedene Übungstypen (transformation, multiple_choice)
3. **Audio hinzufügen:**
- Füge Audio-URLs zu Konversations-Lektionen hinzu
- Verlinke zu YouTube-Videos oder eigenen Audio-Dateien
4. **Kurs teilen:**
- Nutze den Share-Code, um den Kurs mit anderen zu teilen
- Oder mache den Kurs öffentlich
## Lernstrategie
### Täglich:
1. **Vokabeln lernen** (20-25 Min)
- Nutze die Vokabeltrainer-Funktion
- Wiederhole bis 85% erreicht sind
2. **Konversation üben** (15-20 Min)
- Höre Audio
- Sprich laut nach
- Übe mit Muttersprachlern (HelloTalk, Tandem)
3. **Grammatik verstehen** (wenn vorhanden, 20-25 Min)
- Mache die Grammatik-Übungen
- Verstehe die Regeln
### Wöchentlich:
- Wiederholungslektion am Ende jeder Woche
- Vokabeltest am Ende jeder Woche
- Prüfe deinen Fortschritt
### Am Ende:
- Abschlussprüfung
- Kulturelle Tipps lesen
- Weiter üben mit Muttersprachlern!
Viel Erfolg beim Lernen! 🇵🇭

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