246 Commits

Author SHA1 Message Date
Torsten Schulz (local)
b59526b20d feat(Localization, User Management): add missingUserId translations and enhance user ID handling
All checks were successful
Deploy to production / deploy (push) Successful in 2m2s
- Added translations for the "missingUserId" key in multiple languages to improve user feedback when user ID is not available.
- Updated the user ID handling in the store and WorkerSchedulesView component to ensure proper user identification when sending data to the daemon.
- Enhanced error handling to notify users when the user ID is missing, improving overall user experience.
2026-05-08 11:09:34 +02:00
Torsten Schulz (local)
008cd7ae86 feat(Navigation, UserRights, Localization): add worker schedules feature and enhance access control
All checks were successful
Deploy to production / deploy (push) Successful in 1m52s
- Updated navigation structure to include a new section for worker schedules, accessible to specific user roles.
- Introduced a new user right type for 'worker_schedule_read' to manage access permissions effectively.
- Added localization entries for worker schedules in multiple languages, ensuring consistent user experience across the application.
- Created a new route and component for managing worker schedules in the admin panel.
2026-05-08 08:54:17 +02:00
Torsten Schulz (local)
0f7220d0b1 feat(FalukantService): update election filtering to include future dates and enhance eligibility checks
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Modified the election query to filter for upcoming election dates instead of past ones, ensuring only future positions are considered.
- Added checks for prerequisites in election eligibility, allowing positions without prerequisites to be selectable, improving user experience in the application.
2026-05-08 08:30:03 +02:00
Torsten Schulz (local)
0e572f8cbe feat(FalukantService, MessagesDialog): enhance character data handling in notifications
All checks were successful
Deploy to production / deploy (push) Successful in 2m3s
- Added mappings for character titles and genders in the FalukantService to enrich notifications with additional character information.
- Updated the MessagesDialog component to utilize the new title and gender data, improving the display of director names based on their titles and genders.
- Ensured that character names are displayed correctly, enhancing the overall user experience in notifications.
2026-05-08 08:25:36 +02:00
Torsten Schulz (local)
cc89fd4bef feat(VocabPracticeDialog, VocabCourseView): implement event dispatch for hard vocabulary changes
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Added event dispatching for 'yourpart:hardvocab:changed' in both VocabPracticeDialog and VocabCourseView components to notify changes in hard vocabulary items.
- Implemented event handling in VocabCourseView to refresh the hard vocabulary list when the event is triggered, ensuring UI consistency across components.
- Included error handling for environments that do not support CustomEvent, enhancing robustness.
2026-05-07 13:52:49 +02:00
Torsten Schulz (local)
42d0652e48 feat(Director): add autoAdjustIncome feature and enhance director data handling
All checks were successful
Deploy to production / deploy (push) Successful in 1m54s
- Introduced a new boolean field `autoAdjustIncome` in the Director model to manage income adjustments automatically.
- Updated the FalukantService to include `autoAdjustIncome` in director data responses and settings management.
- Enhanced the DirectorInfo component to allow users to toggle the `autoAdjustIncome` setting.
- Updated internationalization files to include translations for the new feature across multiple languages.
2026-05-07 13:29:09 +02:00
Torsten Schulz (local)
ddd8ca49d8 feat(FamilyView): enhance character display and avatar handling
All checks were successful
Deploy to production / deploy (push) Successful in 1m54s
- Refactored character display for spouses, children, and lovers to improve UI consistency and visual appeal.
- Introduced a new method for calculating character avatar styles based on age and gender, enhancing the representation of characters.
- Updated the layout to include compact character media for children and lovers, improving space utilization and readability.
2026-05-07 11:15:23 +02:00
Torsten Schulz (local)
8aeefccc3b feat(FalukantService, FamilyView): enhance marriage satisfaction handling
All checks were successful
Deploy to production / deploy (push) Successful in 1m56s
- Added logic to initialize marriage satisfaction to 100 for newly married couples in the FalukantService.
- Updated the FamilyView component to conditionally display marriage satisfaction only for married relationships, improving clarity in the UI.
- Ensured default values are used when marriage satisfaction is not explicitly set, enhancing user experience.
2026-05-07 09:40:58 +02:00
Torsten Schulz (local)
2c453a4a6b feat(VocabService): enhance logging and error handling in lesson assistant message flow
All checks were successful
Deploy to production / deploy (push) Successful in 2m18s
- Introduced detailed logging throughout the sendLessonAssistantMessage method to track request lifecycle, including start, abort conditions, and response handling.
- Improved error handling for various scenarios such as disabled assistant, unconfigured settings, empty messages, and fetch failures, providing clearer feedback to users.
- Added logging for response parsing and upstream errors to facilitate debugging and improve overall service reliability.
2026-05-07 08:58:22 +02:00
Torsten Schulz (local)
cfab56f63d feat(VocabPracticeDialog): improve SRS handling for incorrect answers
All checks were successful
Deploy to production / deploy (push) Successful in 2m8s
- Introduced logic to reinsert incorrectly answered items into the SRS queue, allowing them to be reviewed again before the session ends.
- Added a constant for the reinsert offset to control the position of requeued items.
- Updated session management to ensure incorrect answers are not marked as completed, enhancing the learning experience.
2026-05-07 08:46:33 +02:00
Torsten Schulz (local)
ab3e8d14e5 feat(AppHeader): improve ad display logic and responsiveness
All checks were successful
Deploy to production / deploy (push) Successful in 2m58s
- Enhanced the AppHeader component to support separate ad slots for mobile and desktop views, improving ad display based on viewport width.
- Introduced a responsive design that adjusts ad dimensions and formats for better visibility across different screen sizes.
- Updated methods to dynamically determine the active ad slot and handle viewport resizing events for optimal ad rendering.
2026-05-06 15:53:58 +02:00
Torsten Schulz (local)
5993f79e7a feat(VocabService, AppHeader): enhance timeout configuration and update ad display settings
All checks were successful
Deploy to production / deploy (push) Successful in 2m21s
- Updated the VocabService to allow configurable timeout for responses, defaulting to 300 seconds if not set or invalid.
- Modified AppHeader component to change ad format to horizontal and adjusted responsive settings for better display consistency.
- Ensured proper height settings for ad elements to maintain layout integrity across different screen sizes.
2026-05-06 15:50:14 +02:00
Torsten Schulz (local)
b1365dccbb feat(MoneyHistoryView): load branch names for enhanced activity display
All checks were successful
Deploy to production / deploy (push) Successful in 2m12s
- Added a new method to load branch names from the API and map them by ID for better display in money history activities.
- Updated the component's data structure to include a mapping of branch names, improving the clarity of tax-related activity translations.
2026-05-06 13:52:44 +02:00
Torsten Schulz (local)
511146da74 feat(FalukantService): add numeric value conversion for money checks
All checks were successful
Deploy to production / deploy (push) Successful in 2m12s
- Introduced a new private method `_toNumericValue` to handle conversion of various input types to numeric values, ensuring proper validation of user money and requirement values.
- Updated `checkMoneyRequirement` method to utilize the new conversion logic, improving accuracy in money requirement checks by handling edge cases for non-finite values.
2026-05-05 08:03:05 +02:00
Torsten Schulz (local)
3365f1dd2a feat(MoneyHistoryView): enhance activity translation for tax and reputation actions
All checks were successful
Deploy to production / deploy (push) Successful in 2m7s
- Added translation logic for activities related to tax from sales branches and reputation actions in the MoneyHistoryView component.
- Updated internationalization files to include new strings for these activities in multiple languages.
2026-04-30 15:41:00 +02:00
Torsten Schulz (local)
c6ffdd10f7 feat(update-frontend): enhance environment file handling and add Google AdSense script
All checks were successful
Deploy to production / deploy (push) Successful in 2m3s
- Updated the script to prioritize the root environment file for builds, falling back to the frontend environment file if necessary.
- Added a Google AdSense script to the frontend HTML for improved monetization capabilities.
2026-04-27 16:36:38 +02:00
Torsten Schulz (local)
530855e26e feat(Moderation): enhance moderation reporting and user feedback
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Added user blocking checks in authentication and reporting processes, returning appropriate error responses.
- Expanded moderation report functionality to include new target types and optional fields for reports.
- Implemented a new API endpoint to retrieve the count of open moderation reports.
- Enhanced frontend components to allow users to report profiles, images, and guestbook entries, with corresponding UI updates.
- Updated internationalization files to include new strings for reporting features in both German and English.
2026-04-27 15:57:02 +02:00
Torsten Schulz (local)
e94ae4350d feat(RandomChatDialog): enhance chat reporting functionality
All checks were successful
Deploy to production / deploy (push) Successful in 2m1s
- Added logic to enable the reporting button based on chat state and message presence.
- Introduced a computed property to determine if the chat can be reported.
- Updated the reporting method to accommodate the last partner in case the current partner is unavailable.
- Improved handling of partner details in the reporting payload for better accuracy.
2026-04-27 15:23:48 +02:00
Torsten Schulz (local)
ff68fb72c4 feat(Chat): implement chat incident reporting feature
All checks were successful
Deploy to production / deploy (push) Successful in 1m57s
- Added reportChatIncident method in ChatController to handle reporting of chat incidents.
- Introduced a new API route for reporting incidents in chatRouter.
- Implemented chatService methods to ensure the chat report table is created and to handle incident data storage.
- Enhanced frontend components to allow users to report incidents in both multi and random chat dialogs.
- Updated internationalization files to include new strings for reporting functionality in multiple languages.
2026-04-27 15:00:52 +02:00
Torsten Schulz (local)
90e1c0496a feat(ModerationReports): enhance moderation reports functionality and UI
All checks were successful
Deploy to production / deploy (push) Successful in 2m3s
- Added topic and forum IDs to moderation report queries for better context.
- Updated admin interface to include links to open reported forum messages.
- Implemented methods to handle opening target messages directly from the moderation reports view.
- Enhanced internationalization support for new UI elements in both German and English.
- Improved scrolling behavior to focus on specific messages when navigating to them.
2026-04-27 14:55:23 +02:00
Torsten Schulz (local)
a02fe1f008 feat(Moderation): implement moderation reports feature
All checks were successful
Deploy to production / deploy (push) Successful in 2m1s
- Added moderationRouter to handle moderation-related API routes.
- Introduced new methods in AdminController for fetching all regions, region types, and creating regions.
- Enhanced adminRouter with routes for moderation reports and status updates.
- Updated navigationController to include moderation reports in the admin menu.
- Implemented frontend components for reporting messages in the forum and managing moderation reports.
- Added internationalization support for moderation-related texts in multiple languages.
2026-04-27 14:52:19 +02:00
Torsten Schulz (local)
7fc9b55b59 feat(FalukantService, CreateBranchDialog): implement dynamic branch cost calculation
All checks were successful
Deploy to production / deploy (push) Successful in 2m8s
- Added new constants and methods in FalukantService to calculate branch action costs based on owned branch types, enhancing the cost management logic.
- Updated the CreateBranchDialog to utilize the new cost calculation, ensuring accurate display of branch creation costs based on user-owned branches.
- Improved the handling of branch type costs, allowing for a more flexible and responsive user experience when creating and upgrading branches.
2026-04-27 14:35:45 +02:00
Torsten Schulz (local)
d854200708 feat(VocabPracticeDialog, VocabCourseView): enhance hard vocabulary management and UI
All checks were successful
Deploy to production / deploy (push) Successful in 2m6s
- Refactored methods in VocabPracticeDialog to improve the handling of hard vocabulary items, including the addition of `findMatchingHardKey` and `removeHardEntriesForItem` for better management of hard vocabulary entries.
- Updated the VocabCourseView to display a new section for hard vocabulary items, allowing users to view and remove difficult words easily.
- Enhanced the UI with new styles for the hard vocabulary list, improving user engagement and accessibility to challenging vocabulary practice.
2026-04-26 23:32:18 +02:00
Torsten Schulz (local)
7fd8e4dda8 fix(VocabPracticeDialog): adjust dialog dimensions for improved layout
All checks were successful
Deploy to production / deploy (push) Successful in 1m54s
- Increased the width from 55em to 64em and height from 32em to 40em to enhance the visual layout of the VocabPracticeDialog component, providing a better user experience.
2026-04-23 14:25:24 +02:00
Torsten Schulz (local)
0e39ca9a0f feat(VocabPracticeDialog): improve vocabulary item normalization and expansion
All checks were successful
Deploy to production / deploy (push) Successful in 1m48s
- Enhanced the normalization process for vocabulary items to better accommodate multiple valid translations, improving the overall accuracy of vocabulary practice.
- Updated methods for expanding vocabulary item alternatives, ensuring a more flexible and effective learning experience for users.
2026-04-23 13:47:21 +02:00
Torsten Schulz (local)
79fe05c630 feat(VocabPracticeDialog): enhance vocabulary item expansion logic
All checks were successful
Deploy to production / deploy (push) Successful in 1m48s
- Added methods to split phrase alternatives and expand pool item alternatives, allowing for better handling of multiple valid translations and maintaining alignment between learning and reference phrases.
- Updated the normalization process to incorporate these new methods, improving the accuracy and flexibility of vocabulary practice sessions.
2026-04-23 13:44:31 +02:00
Torsten Schulz (local)
e3c024d5af feat(VocabPracticeDialog): enhance answer variant expansion logic
All checks were successful
Deploy to production / deploy (push) Successful in 1m51s
- Renamed `expandAnswerVariants` to `expandSingleAnswerVariants` for clarity and added a new `expandAnswerVariants` method to handle full-answer alternatives like "A / B".
- Improved the logic to ensure that phrase alternatives are expanded correctly, enhancing the accuracy of answer variant generation during vocabulary practice.
2026-04-23 13:40:08 +02:00
Torsten Schulz (local)
783dd175e8 feat(VocabCourseView): add hard vocabulary practice button and refresh logic
All checks were successful
Deploy to production / deploy (push) Successful in 1m57s
- Introduced a button to start hard vocabulary practice when applicable, enhancing user engagement with challenging vocabulary.
- Implemented methods to manage and refresh the hard vocabulary list from local storage, ensuring users have access to their difficult words.
- Added event listeners to refresh the hard vocabulary list on window focus, improving the overall user experience.
2026-04-23 13:10:05 +02:00
Torsten Schulz (local)
cab5428d0b feat(VocabLessonView): enhance vocab trainer visibility for cross-chapter hard vocabulary
All checks were successful
Deploy to production / deploy (push) Successful in 1m58s
- Updated the VocabLessonView to display the vocab trainer section when there are cross-chapter hard vocabulary items, improving user engagement and accessibility to training options.
- Adjusted the button rendering logic to ensure that the hard vocabulary trainer is available when applicable, enhancing the overall learning experience.
2026-04-23 10:04:46 +02:00
Torsten Schulz (local)
478a7ffc96 fix(VocabPracticeDialog): adjust success tracking for hard-phase drills
All checks were successful
Deploy to production / deploy (push) Successful in 1m56s
- Updated the success tracking logic to prevent inflating normal success counters during hard-phase drills, ensuring accurate representation of user progress.
- Introduced a condition to separate tracking for hard-phase activities, enhancing the integrity of vocabulary rotation and user experience.
2026-04-22 11:55:40 +02:00
Torsten Schulz (local)
677e4c674e refactor(VocabPracticeDialog): replace hard-coded consecutive correct threshold with constant
All checks were successful
Deploy to production / deploy (push) Successful in 1m56s
- Introduced a constant `HARD_REQUIRED_CONSECUTIVE_CORRECT` to define the required number of consecutive correct answers for hard vocabulary items, improving code maintainability and readability.
- Updated relevant methods to utilize the new constant, ensuring consistent behavior across the component.
2026-04-22 11:51:49 +02:00
Torsten Schulz (local)
d3aad6e7ef feat(localization): add difficult vocabulary marking features in multiple languages
All checks were successful
Deploy to production / deploy (push) Successful in 1m58s
- Introduced new localization strings for marking vocabulary as difficult, including options to save and remove difficult marks.
- Enhanced user experience by providing feedback on the status of difficult vocabulary in Cebuano, Spanish, and French.
- Updated existing localization files to ensure consistency across languages, supporting the recent vocabulary management features.
2026-04-22 11:12:26 +02:00
Torsten Schulz (local)
44b40d5a46 feat(VocabPracticeDialog, localization): add hard vocabulary marking feature
All checks were successful
Deploy to production / deploy (push) Successful in 1m53s
- Introduced functionality to mark vocabulary items as difficult, allowing users to track and manage challenging words during practice sessions.
- Added buttons for marking and unmarking current vocabulary as hard, enhancing user engagement and learning effectiveness.
- Updated the UI to display the count of hard-marked vocabulary items, providing users with better insights into their learning progress.
- Enhanced localization files for German and English to support new vocabulary marking features, ensuring a consistent user experience across languages.
2026-04-21 15:57:21 +02:00
Torsten Schulz (local)
4cc2aace6b feat(DayProduction, FalukantService, VocabLessonView): enhance vocabulary training and production tracking
All checks were successful
Deploy to production / deploy (push) Successful in 1m51s
- Added a new `completionCount` field to the DayProduction model to track the number of completed productions.
- Updated the FalukantService to aggregate completed productions using the new `completionCount` field, improving accuracy in production statistics.
- Introduced new vocabulary training features in VocabLessonView, including options to mark vocabulary as difficult and track remaining hard vocabulary, enhancing user engagement and learning effectiveness.
- Updated localization files for German and English to support new vocabulary training features, ensuring a consistent user experience across languages.
2026-04-21 15:44:44 +02:00
Torsten Schulz (local)
27d42c0a3a fix(AdminService, AdminController): enhance error handling and character deletion logic
All checks were successful
Deploy to production / deploy (push) Successful in 1m44s
- Updated error handling in AdminController to include 'targetnotdead' status for improved response accuracy.
- Modified character deletion logic in AdminService to ensure correct target character is identified, especially when dealing with living characters, enhancing data integrity during cleanup operations.
- Adjusted attributes fetched for characters to include 'health', facilitating better decision-making in deletion processes.
2026-04-20 16:27:08 +02:00
Torsten Schulz (local)
7417736daf refactor(AdminService, EditUserView): streamline character data deletion and enhance confirmation dialog
All checks were successful
Deploy to production / deploy (push) Successful in 1m46s
- Introduced a reusable `deleteIfTableExists` function in AdminService to simplify the deletion of character-related data across multiple tables, improving code maintainability.
- Updated the deletion logic to check for table existence before executing delete queries, enhancing robustness.
- Enhanced the EditUserView by integrating a `ChooseDialog` for user confirmation before proceeding with character death cleanup, improving user experience and interaction clarity.
2026-04-20 16:16:48 +02:00
Torsten Schulz (local)
267711fca6 feat(AdminController, AdminService, AdminRouter, localization): add character death cleanup feature
All checks were successful
Deploy to production / deploy (push) Successful in 1m52s
- Implemented `adminCleanupCharacterDeathArtifacts` method in AdminService to remove stale character-bound data after death/inheritance, including knowledge, debtors prism, and political offices.
- Added corresponding route in AdminRouter for triggering the cleanup process via an API endpoint.
- Enhanced AdminController to handle requests for the new cleanup feature, ensuring proper error handling and response formatting.
- Updated frontend components to include a user interface for initiating the cleanup, with localization support in both English and German for improved user experience.
2026-04-20 15:43:44 +02:00
Torsten Schulz (local)
8ce15441bf feat(VocabPracticeDialog): implement immediate retry feature for incorrect answers
All checks were successful
Deploy to production / deploy (push) Successful in 1m47s
- Added a `pendingRetry` property to manage immediate retries after incorrect answers, enhancing user experience during vocabulary practice.
- Updated logic to handle retries, allowing users to quickly attempt the last question again with the correct direction.
- Ensured that the retry state is reset appropriately after correct answers or when skipping questions, maintaining clarity in the training flow.
2026-04-20 10:02:32 +02:00
Torsten Schulz (local)
1500f01875 feat(VocabPracticeDialog, localization): implement last wrong answer review feature
All checks were successful
Deploy to production / deploy (push) Successful in 1m49s
- Added a new section in VocabPracticeDialog to display the last incorrect answer, including the asked vocabulary, user answer, and correct solutions.
- Updated localization strings in Cebuano, German, English, Spanish, and French to support the new review feature, enhancing user experience across multiple languages.
- Improved UI layout for the review section to provide clearer feedback during vocabulary training sessions.
2026-04-20 09:52:26 +02:00
Torsten Schulz (local)
5f13583e41 feat(VocabLessonView, localization): add vocabulary review feature and update translations
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Implemented a new vocabulary review section in VocabLessonView to display the last incorrect answer, including the asked vocabulary, user answer, and correct answer.
- Added localization strings for the new review feature in Cebuano, German, English, Spanish, and French, enhancing user experience across multiple languages.
- Updated the UI to visually differentiate the review section, improving clarity and usability during vocabulary training sessions.
2026-04-20 09:39:48 +02:00
Torsten Schulz (local)
dee4991be7 feat(VocabLessonView): implement vocab trainer continue timer management
All checks were successful
Deploy to production / deploy (push) Successful in 2m2s
- Added a new timer management feature to the vocab trainer, allowing for better control over the continuation of questions after user answers.
- Introduced `clearVocabTrainerContinueTimer` method to prevent multiple timers from running simultaneously, enhancing the stability of the training session.
- Updated various methods to utilize the new timer management, ensuring a smoother user experience during vocabulary training.
2026-04-20 09:26:02 +02:00
Torsten Schulz (local)
d7fa5f925e feat(VocabService, localization, VocabLessonView): extend assistant wait feedback and timeout duration
All checks were successful
Deploy to production / deploy (push) Successful in 2m2s
- Increased the timeout duration for the language assistant from 30 seconds to 120 seconds to accommodate longer processing times.
- Added new localization strings across multiple languages (Cebuano, German, English, Spanish, French) to provide user feedback during the assistant's wait period, enhancing clarity and user experience.
- Implemented a visual waiting indicator in VocabLessonView to inform users that the assistant is still processing their request, improving overall interaction quality.
2026-04-20 08:58:51 +02:00
Torsten Schulz (local)
e28ed7bdb5 feat(VocabService, VocabPracticeDialog, VocabLessonView): enhance vocabulary training logic and UI feedback
All checks were successful
Deploy to production / deploy (push) Successful in 2m17s
- Introduced methods for improved text analysis and validation in VocabService, including `_wordCount` and `_looksLikeFragmentMismatch`, to better assess learning and reference pairs.
- Updated VocabPracticeDialog to display submitted answers and correct solutions, enhancing user feedback during practice sessions.
- Enhanced VocabLessonView to ensure only trainable vocabulary pairs are processed, improving the quality of vocabulary training.
- Added localization entries for new UI elements in both English and German, ensuring clarity in user interactions.
2026-04-20 08:48:39 +02:00
Torsten Schulz (local)
553f132184 feat(VocabPracticeDialog): add answer variant expansion for improved vocabulary training
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Introduced a new method `expandAnswerVariants` to generate multiple answer variants from a given input, enhancing the flexibility of acceptable answers.
- Improved handling of words with slashes to create diverse answer options, ensuring a richer vocabulary training experience.
- Updated the answer collection logic to utilize the new method, allowing for a broader range of valid responses during practice sessions.
2026-04-20 08:36:11 +02:00
Torsten Schulz (local)
f64c923db6 feat(VocabService, VocabPracticeDialog): enhance text analysis and SRS validation
All checks were successful
Deploy to production / deploy (push) Successful in 1m50s
- Improved text analysis methods in VocabService to better identify instructional-like texts and task-related prompts, enhancing vocabulary training quality.
- Updated VocabPracticeDialog to include a new method for checking instruction-like text, ensuring more accurate filtering of trainable pairs.
- Incremented SRS session storage version to reflect changes in session management and data handling.
- Refactored normalization and validation logic to maintain consistency across vocabulary entries and improve user experience.
2026-04-20 08:30:58 +02:00
Torsten Schulz (local)
71f2ee7c97 feat(seo, routing): add Bisaya learning and German for Bisaya courses
All checks were successful
Deploy to production / deploy (push) Successful in 2m0s
- Introduced new routes for Bisaya learning and German for Bisaya courses, enhancing the marketing section of the application.
- Updated sitemap.xml to include new course URLs for better SEO visibility.
- Added localization entries for both courses in English and German, improving content accessibility for users.
- Enhanced SEO metadata generation for the new courses, ensuring proper indexing and visibility in search engines.
- Updated VocabLandingView to feature links to the new courses, improving user navigation and engagement.
2026-04-20 08:28:00 +02:00
Torsten Schulz (local)
e6c90c219b feat(VocabService): improve SRS pair validation and introduce new text analysis methods
All checks were successful
Deploy to production / deploy (push) Successful in 2m13s
- Refactored SRS pair validation logic by introducing the `_isTrainableSrsPair` method to enhance the criteria for acceptable learning and reference pairs.
- Added `_isInstructionLikeText` method to filter out instructional texts from SRS pairs, improving the quality of vocabulary training.
- Updated various methods to utilize the new validation logic, ensuring consistent handling of vocabulary entries across the service.
- Enhanced the `_courseHasDueSrsItems` and `_extractTrainerVocabsFromLessonDidactics` methods to improve data retrieval and filtering based on the new criteria.
2026-04-20 08:21:18 +02:00
Torsten Schulz (local)
b6d749f781 feat(VocabLessonView): enhance vocab trainer logic and answer normalization
All checks were successful
Deploy to production / deploy (push) Successful in 1m51s
- Refactored vocab trainer logic to improve handling of acceptable answers, allowing for multiple translations and synonyms.
- Introduced a new method to retrieve equivalent vocabulary answers, ensuring mixed pools from different lessons provide valid alternatives.
- Enhanced answer normalization process to prevent duplicates and improve the quality of acceptable answers presented to users.
2026-04-17 17:30:03 +02:00
Torsten Schulz (local)
39dcdd7fb3 feat(FalukantService, OverviewView): enhance church office information retrieval and UI display
All checks were successful
Deploy to production / deploy (push) Successful in 1m51s
- Refactored the `getHighestChurchOfficeInfo` method to utilize a new approach for retrieving the highest church office, improving accuracy and performance.
- Updated the `OverviewView` to format and display the highest political and church office names instead of raw rank values, enhancing user experience and clarity.
- Introduced a new method `formatCertificateHighestOffice` for better localization and presentation of office titles in the UI.
2026-04-17 17:17:17 +02:00
Torsten Schulz (local)
afbea926a2 feat(FalukantService, i18n): enhance household tension handling and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Added functionality to refresh and update household tension scores and reasons in the FalukantService, improving user experience by reflecting real-time household dynamics.
- Updated localization strings across multiple languages (Cebuano, German, English, Spanish, French) to include new office rank terminology and improved marriage crisis descriptions, enhancing clarity and context for users.
- Introduced a new office rank tier structure in localization files, providing better categorization and understanding of office ranks in the game.
2026-04-17 17:07:53 +02:00
Torsten Schulz (local)
1dd4d18927 fix(FalukantService): update userHouse query attributes and transaction handling
All checks were successful
Deploy to production / deploy (push) Successful in 1m52s
- Modified the userHouse query to include 'id' in the attributes, improving data retrieval.
- Enhanced transaction handling in state and userHouse updates to ensure atomic operations, preventing potential data inconsistencies.
2026-04-17 16:58:41 +02:00
Torsten Schulz (local)
d2eebf1f94 feat(VocabService, VocabCourseView): enhance SRS item retrieval and UI integration
All checks were successful
Deploy to production / deploy (push) Successful in 1m52s
- Updated VocabService to calculate total due count for spaced repetition system (SRS) items, improving data handling for user queries.
- Modified VocabCourseView to incorporate total due count in the UI, ensuring accurate display of SRS item statistics.
- Enhanced error handling and data validation for SRS due items, improving overall reliability and user experience.
2026-04-17 16:53:56 +02:00
Torsten Schulz (local)
7732498875 feat(MiniCalendarWidget): enhance localization and refactor calendar logic
All checks were successful
Deploy to production / deploy (push) Successful in 1m55s
- Updated the MiniCalendarWidget to utilize localized strings for month names, weekdays, and loading messages, improving user experience across multiple languages.
- Refactored the calendar logic to use constants for month keys and weekday order, ensuring consistency with backend configurations.
- Added new translation entries for the mini widget in Cebuano, German, English, Spanish, and French, enhancing accessibility for users.
2026-04-17 16:44:27 +02:00
Torsten Schulz (local)
14881803df feat(i18n): enhance localization for chat and minigames
All checks were successful
Deploy to production / deploy (push) Successful in 1m51s
- Updated localization strings for chat and minigames across multiple languages, including Cebuano, German, English, Spanish, and French, to improve user experience and clarity.
- Added new translations for participant counts, user selection prompts, and game over messages, ensuring consistency and better engagement in the UI.
- Enhanced existing translations for accuracy and context, particularly in the TaxiGame component and chat functionalities.
2026-04-17 16:38:39 +02:00
Torsten Schulz (local)
3232e42251 feat(VocabPracticeDialog): implement SRS session management and enhance UI feedback
All checks were successful
Deploy to production / deploy (push) Successful in 1m53s
- Added support for spaced repetition system (SRS) session management, allowing users to resume practice sessions with due items.
- Introduced new UI elements to display SRS session status, including total due, completed, and remaining items.
- Enhanced localization for SRS-related messages in multiple languages, improving user experience and clarity.
- Updated methods for saving and loading SRS session data to local storage, ensuring persistence across sessions.
2026-04-17 16:28:19 +02:00
Torsten Schulz (local)
9c121d2dc2 feat(i18n): update Cebuano localization for minigames and enhance UI text
All checks were successful
Deploy to production / deploy (push) Successful in 1m56s
- Added new localized strings for minigames in Cebuano, including loading hints, objectives, and play focus descriptions to improve user experience.
- Updated existing translations for clarity and consistency across various game elements.
- Enhanced the Match3Game component to utilize localized strings, ensuring dynamic text rendering based on user language preferences.
- Included a new entry in .gitignore for the locale audit report to maintain a clean repository.
2026-04-17 16:20:20 +02:00
Torsten Schulz (local)
71d5922409 feat(bisaya-course): refine phase 4 didactics and enhance course content generation
All checks were successful
Deploy to production / deploy (push) Successful in 5m19s
- Corrected grammatical errors and improved the phrasing in the BISAYA_PHASE4_DIDACTICS, ensuring clarity and accuracy in the learning materials.
- Updated the course content generation script to include lessons from phase 5, enhancing the overall structure and flow of the course.
- Introduced a new vocabulary course content synchronization process, improving the integration of vocabulary resources across different modules.
- Enhanced the VocabService to dynamically adjust temperature settings based on the mode, optimizing response generation for different contexts.
- Added new localized titles and vocabulary entries in multiple languages, enriching the learning experience for users.
2026-04-17 16:00:41 +02:00
Torsten Schulz (local)
5c315c477f feat(i18n): add language trainer SEO content and update home page metadata
All checks were successful
Deploy to production / deploy (push) Successful in 5m2s
- Introduced a new section for language trainers in multiple languages (Cebuano, German, English, Spanish, French) to enhance user guidance on beginner courses.
- Updated home page SEO metadata to reflect the addition of language courses, improving search visibility and user engagement.
- Enhanced the NoLoginView component to display the new language trainer information, providing users with clear insights into available resources.
2026-04-17 15:30:12 +02:00
Torsten Schulz (local)
776dea2584 feat(LanguageAssistantView): add preset options for Ollama and OpenAI
All checks were successful
Deploy to production / deploy (push) Successful in 4m31s
- Introduced a new section for language assistant presets, allowing users to quickly apply configurations for Ollama and OpenAI.
- Implemented methods to set the base URL and model for each preset, enhancing user experience with one-click setup.
- Added styling for the preset buttons and hints to improve UI clarity and usability.
2026-04-17 15:24:22 +02:00
Torsten Schulz (local)
4205639de3 feat(bisaya-course): enhance phase 4 didactics with new learning goals and speaking prompts
All checks were successful
Deploy to production / deploy (push) Successful in 1m31s
- Expanded the BISAYA_PHASE4_DIDACTICS by adding new learning goals, core patterns, speaking prompts, and practical tasks to improve language acquisition.
- Updated the course content generation scripts to incorporate the new lesson structures and ensure alignment with the latest didactic updates.
- Enhanced the logic for generating exercises based on lesson types, introducing additional situational and speaking exercises for advanced lessons.
2026-04-17 15:12:21 +02:00
Torsten Schulz (local)
4df8f97a41 feat(bisaya-course): add new 'Ort & Richtung' section with learning goals and practical tasks
All checks were successful
Deploy to production / deploy (push) Successful in 4m46s
- Introduced a new section for 'Ort & Richtung' in the Bisaya didactics, including learning goals, core patterns, grammar focus, speaking prompts, and practical tasks to enhance language learning.
- Updated the lesson didactics to include the new section, ensuring comprehensive coverage of location and direction vocabulary.
2026-04-17 14:51:09 +02:00
Torsten Schulz (local)
f93a0f8b35 feat(VocabLessonView): enhance review lesson logic and vocabulary handling
All checks were successful
Deploy to production / deploy (push) Successful in 4m28s
- Added logic to adjust vocabulary blending and switching conditions based on lesson type and didactic mode, improving the learning experience during review lessons.
- Implemented dynamic thresholds for switching between multiple choice and typing modes based on success rates and lesson context.
- Enhanced vocabulary source selection to incorporate a review share, optimizing the question pool for better engagement during typing sessions.
2026-04-17 14:37:40 +02:00
Torsten Schulz (local)
b3346d4cac fix(update-backend): adjust file permissions and handle environment variables safely
All checks were successful
Deploy to production / deploy (push) Successful in 4m25s
- Updated the permission settings for the environment file to 640, ensuring it is readable only by the owner and the deploy group.
- Modified the sequelize configuration to safely handle missing environment variables, converting them to strings or setting them to undefined to prevent runtime errors.
[force-deploy]
2026-04-17 13:53:13 +02:00
Torsten Schulz (local)
1b9d9664b2 refactor(update-backend): reorder permission settings and update migration message
Some checks failed
Deploy to production / deploy (push) Failing after 3m20s
- Moved permission setting commands to the end of the script for better clarity and execution flow.
- Changed the log message from "Running database synchronization..." to "Running database migrations..." to accurately reflect the operation being performed.
- Ensured that permission settings for backend and data directories are consistently applied after the migration process.
[force-deploy]
2026-04-17 13:45:53 +02:00
Torsten Schulz (local)
9a92940dab chore(package): update version to 3.0.0-beta-1.0.0 in package.json
Some checks failed
Deploy to production / deploy (push) Failing after 3m22s
[force-deploy]
2026-04-17 13:41:00 +02:00
Torsten Schulz (local)
5070785a50 feat(deploy): update deployment workflow and migration paths
Some checks failed
Deploy to production / deploy (push) Has been cancelled
- Modified the deployment workflow to include new migration paths for the backend, ensuring that migrations are correctly referenced in the deployment process.
- Updated the `db:migrate` script in package.json to point to the `migrations-active` directory, enhancing clarity and organization of migration files.
- Adjusted the deployment conditions to account for changes in migration file locations, improving the accuracy of change detection during deployments.
- Removed obsolete migration files to streamline the migration process and prevent confusion.
2026-04-17 13:39:38 +02:00
Torsten Schulz (local)
70c381114b feat(migration): enhance contact_message migration to check for existing columns before adding/removing
Some checks failed
Deploy to production / deploy (push) Failing after 3m14s
- Updated the migration script for the contact_message table to conditionally add or remove columns based on their existence, improving migration safety and preventing errors during deployment.
- This change ensures that the migration can be run multiple times without causing issues if the columns already exist or are missing.
[force-deploy]
2026-04-17 12:01:25 +02:00
Torsten Schulz (local)
de9f2c853d feat(deploy): add force deploy detection to deployment workflow
Some checks failed
Deploy to production / deploy (push) Failing after 3m17s
- Implemented logic to check for a `[force-deploy]` tag in the latest commit message, allowing for a forced deployment regardless of detected changes.
- Updated deployment conditions to include force deploy status, enabling more flexible deployment options based on commit messages.
- Enhanced sync step to trigger on force deploy, ensuring content updates are applied even without file changes.
2026-04-17 11:53:00 +02:00
Torsten Schulz (local)
1e9e247dec feat(update-backend): streamline dependency management and migration process
All checks were successful
Deploy to production / deploy (push) Successful in 1m14s
- Updated the npm install command to remove the production flag, allowing for the installation of all dependencies.
- Added a step to prune development dependencies after running database migrations, optimizing the deployment environment.
2026-04-17 11:42:41 +02:00
Torsten Schulz (local)
7b907ea683 feat(VocabLessonView): improve lesson reset behavior and normalize vocabulary handling
Some checks failed
Deploy to production / deploy (push) Has been cancelled
- Updated lesson reset functionality to ensure users start in the 'learn' tab after resetting progress.
- Enhanced vocabulary normalization by adding a method to strip trailing parenthetical notes, improving text consistency.
- Adjusted normalization logic to conditionally ignore trailing notes based on user input mode, enhancing user experience during vocabulary training.
2026-04-17 11:41:46 +02:00
Torsten Schulz (local)
e96c37aac5 feat(database): add migration and sync-db scripts to deployment process
Some checks failed
Deploy to production / deploy (push) Failing after 3m20s
- Introduced a new npm script for database migrations in package.json.
- Updated update-backend.sh to execute migrations and conditionally run legacy sync-db based on environment variable.
- Enhanced syncDatabase.js to skip initialization routines if APP_INIT_ON_START is not set, promoting best practices for schema changes during deployment.
2026-04-17 11:34:59 +02:00
Torsten Schulz (local)
1f10e7c519 feat(deploy): enhance deployment scripts with skip options for backend and frontend
All checks were successful
Deploy to production / deploy (push) Successful in 1m31s
- Updated `deploy-yourpart-bluegreen.sh` to pass additional arguments for skipping backend or frontend updates.
- Enhanced `update.sh` to handle `--skip-backend` and `--skip-frontend` flags, allowing for more flexible deployment based on changes detected.
- Modified deployment workflow to conditionally execute based on changes in frontend or backend files, improving deployment efficiency.
2026-04-17 11:33:02 +02:00
Torsten Schulz (local)
2461e98fb0 feat(bisaya-course): enhance script execution with direct run detection
All checks were successful
Deploy to production / deploy (push) Successful in 4m25s
- Added functionality to detect if the script is run directly, allowing for conditional execution of the main function.
- Imported `pathToFileURL` from the 'url' module to facilitate the direct run check, improving script usability and flexibility.
2026-04-17 11:23:40 +02:00
Torsten Schulz (local)
3f1b474fdd feat(deploy): add vocab course change detection and sync step
Some checks failed
Deploy to production / deploy (push) Failing after 4m12s
- Implemented a new workflow step to detect changes in vocab course files and conditionally sync content.
- Added a script to check for specific changes in the repository and trigger the sync process if necessary.
- Introduced a new npm script for syncing vocab course content in the backend package.json.
2026-04-17 11:17:59 +02:00
Torsten Schulz (local)
f8f5017436 feat(bisaya-course): enhance lesson structure and didactics integration
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Added an auto-incrementing primary key to the UserHouse model for improved database management.
- Integrated new didactics fragments from the Bisaya course plan, including relationship anchor didactics and lessons for enhanced curriculum depth.
- Updated lesson retrieval logic to utilize planned lesson titles, improving the accuracy of didactics resolution.
- Refactored course content generation scripts to incorporate new didactics, ensuring a comprehensive learning experience for users.
2026-04-17 11:08:12 +02:00
Torsten Schulz (local)
a022b8c174 feat(bisaya-course): add new didactics fragments for everyday conversations
All checks were successful
Deploy to production / deploy (push) Successful in 2m49s
- Introduced new didactics fragments for "Alltagsgespräche - Teil 1" and "Alltagsgespräche - Teil 2," including learning goals, core patterns, speaking prompts, and practical tasks to enhance conversational skills in Bisaya.
- Updated the lesson structure to incorporate these new fragments, improving the overall curriculum and providing learners with more comprehensive resources for everyday interactions.
2026-04-17 10:52:39 +02:00
Torsten Schulz (local)
09a10ff830 feat(bisaya-course, vocabService): expand numerical didactics and enhance lesson progress serialization
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
- Added new glosses for the numbers sixty, seventy, and eighty in the Bisaya didactics, enriching the numerical curriculum.
- Updated the _serializeLessonProgress method to include an options parameter for suppressing lesson review due notifications based on SRS item status.
- Introduced a new method to check for due SRS items, improving the handling of lesson progress and review scheduling.
2026-04-17 09:30:36 +02:00
Torsten Schulz (local)
8be215761d feat(VocabPracticeDialog, VocabCourseView): implement SRS rating feature and enhance user feedback
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Added SRS rating buttons in VocabPracticeDialog to allow users to rate their confidence after answering vocabulary questions.
- Updated methods to handle SRS ratings and integrated them into the review process, improving spaced repetition feedback.
- Enhanced UI with new styles for SRS rating buttons and updated translations for SRS-related terms in multiple languages.
- Modified VocabCourseView to display appropriate introductory text based on SRS due items, improving user guidance.
2026-04-17 09:27:29 +02:00
Torsten Schulz (local)
e2c1147d75 feat(vocab): implement SRS features and enhance vocabulary management
All checks were successful
Deploy to production / deploy (push) Successful in 2m49s
- Added new endpoints in vocabController for retrieving SRS due items and reviewing SRS items, improving spaced repetition support.
- Updated vocabService to handle SRS item creation and scheduling, ensuring effective tracking of vocabulary exposure.
- Enhanced vocabRouter with new routes for SRS functionalities, facilitating user interaction with spaced repetition features.
- Modified VocabPracticeDialog and VocabCourseView to integrate SRS due items, providing users with timely review opportunities.
- Updated translations and UI elements to reflect new SRS features, enhancing user experience and accessibility.
2026-04-17 09:14:30 +02:00
Torsten Schulz (local)
54a77c2e08 feat(vocabService, VocabPracticeDialog, VocabLessonView): enhance vocabulary handling and exposure tracking
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Updated vocabService to merge extracted vocabularies and improve handling of learning and reference pairs.
- Introduced normalization and exposure tracking in VocabPracticeDialog to ensure diverse and underexposed vocabulary practice.
- Enhanced VocabLessonView with methods to identify underexposed vocabularies and adjust selection logic for improved learning outcomes.
- Implemented new constants for minimum exposure requirements to optimize vocabulary training sessions.
2026-04-17 08:58:50 +02:00
Torsten Schulz (local)
d119869750 feat(bisaya-course): expand numerical didactics with additional glosses
All checks were successful
Deploy to production / deploy (push) Successful in 2m45s
- Added new glosses for numbers in Bisaya, enhancing the didactics for numerical lessons.
- Included translations for numbers 4 to 19, improving the comprehensiveness of the numerical curriculum.
- Updated the lesson structure to support the expanded vocabulary, ensuring a richer learning experience.
2026-04-17 08:21:02 +02:00
Torsten Schulz (local)
712370cad3 feat(bisaya-course): add didactics fragments for "Haus & Familie" lessons
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Introduced comprehensive didactics snippets for the "Haus & Familie" lesson, including learning goals, core patterns, grammar focus, speaking prompts, and practical tasks.
- Updated the lesson retrieval logic to incorporate these new fragments as a fallback for core patterns, enhancing the robustness of lesson content delivery.
- Modified the vocab service to utilize the new didactics fragments, ensuring a seamless integration into the existing lesson structure.
2026-04-16 22:44:57 +02:00
Torsten Schulz (local)
229bdc96bf feat(bisaya-course): update lesson retrieval and didactics processing
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Refactored the lesson retrieval logic to include lesson number and title in the query, improving data handling.
- Updated the didactics processing to utilize a lesson-like object instead of fetching lessons by primary key, enhancing performance.
- Improved error handling and ensured proper closure of the database connection after execution.
2026-04-16 22:36:15 +02:00
Torsten Schulz (local)
c1421db72c feat(bisaya-course): add new exercises for situational role plays and enhance lesson normalization
All checks were successful
Deploy to production / deploy (push) Successful in 3m3s
- Introduced a new section titled "Rollenspiele - echte Situationen" with multiple exercises focusing on real-life scenarios in Bisaya, including multiple-choice, gap-fill, and transformation tasks.
- Implemented a normalization function for lesson titles to improve matching accuracy and facilitate the rebuilding of placeholder lessons.
- Updated the course content generation logic to replace outdated exercises with new ones based on normalized titles, ensuring a more relevant learning experience.
2026-04-16 22:28:10 +02:00
Torsten Schulz (local)
68ac5ec281 feat(bisaya-course): add new lesson and update course creation logic
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Introduced a new lesson titled "Zahlen & Preise" to enhance the numerical curriculum in Bisaya.
- Updated the course creation logic to check for existing "Familien"-Bisaya courses, ensuring idempotency in course creation.
- Enhanced the lesson didactics by mapping legacy titles to current lesson keys, improving data consistency.
- Adjusted the course generation script to limit Bisaya courses to German-speaking learners only, streamlining course offerings.
2026-04-16 22:16:23 +02:00
Torsten Schulz (local)
6dce418728 feat(bisaya-course): enhance numerical lessons with expanded content
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Updated lessons on numbers to include detailed sections for "Zahlen 1–20," "Zahlen: Zehner," "Zahlen: Hunderter," and "Zahlen: Tausender," focusing on foundational numerical concepts in Bisaya.
- Introduced various exercise types such as multiple-choice, gap-fill, and transformation tasks to improve learner engagement and comprehension of numerical expressions.
- Enhanced didactics with clear learning goals, core patterns, grammar focus, speaking prompts, and practical tasks to support effective learning.
- Added review lessons to reinforce key concepts and ensure continuity in learning progression.
2026-04-16 21:55:05 +02:00
Torsten Schulz (local)
b2942c1c9d feat(vocabService): enhance answer validation with comparable variants
All checks were successful
Deploy to production / deploy (push) Successful in 2m45s
- Introduced methods to expand comparable answer variants and improve answer equivalence checks, allowing for more flexible user responses.
- Updated the answer validation logic to utilize the new methods, enhancing the accuracy of response evaluation during exercises.
2026-04-16 16:46:37 +02:00
Torsten Schulz (local)
41b07a8951 fix(bisaya-course): update alternatives for transformation exercise
All checks were successful
Deploy to production / deploy (push) Successful in 2m57s
- Modified the alternatives for a transformation exercise to include additional variations, enhancing the response options for learners.
- Updated the vocabulary service to incorporate both primary and alternative answers, improving the accuracy of answer validation during exercises.
2026-04-16 16:42:48 +02:00
Torsten Schulz (local)
9bc6f32b96 feat(bisaya-course): expand lessons on shopping, prices, and numbers
All checks were successful
Deploy to production / deploy (push) Successful in 2m43s
- Added new lessons titled "Einkaufen & Preise" and "Zahlen & Preise" with various exercises focusing on shopping vocabulary and numerical expressions in Bisaya.
- Introduced multiple exercise types including multiple-choice, gap-fill, and transformation tasks to enhance learner engagement and understanding of practical language use in market contexts.
- Updated didactics to include learning goals, core patterns, grammar focus, speaking prompts, and practical tasks related to shopping dialogues and numerical comprehension.
- Included a review lesson "Woche 2 - Wiederholung" to reinforce key concepts from previous lessons, ensuring comprehensive learning continuity.
- Enhanced vocabulary testing with a new "Woche 2 - Vokabeltest" to assess retention of key terms and phrases.
2026-04-16 16:25:47 +02:00
Torsten Schulz (local)
8e29953a95 feat(bisaya-course): add new lesson on time and date concepts
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Introduced a new lesson titled "Zeit & Datum" with multiple exercises focusing on recognizing and using key time-related vocabulary in Bisaya.
- Added various exercise types including multiple-choice, gap-fill, and transformation tasks to enhance learner engagement and understanding of temporal expressions.
- Updated didactics to include learning goals, core patterns, grammar focus, speaking prompts, and practical tasks related to time markers and simple date questions.
2026-04-16 16:09:10 +02:00
Torsten Schulz (local)
e26a3333c4 fix(vocabLessonReview): improve input normalization by trimming punctuation
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s
- Enhanced the `normalize` method to trim trailing punctuation marks (.,!?;:) from input strings, ensuring cleaner and more consistent user input.
- Added logic to replace multiple spaces with a single space, improving the overall formatting of normalized strings.
2026-04-16 15:23:00 +02:00
Torsten Schulz (local)
44850d5913 feat(bisaya-course): update gap-fill exercise structure and instructions
All checks were successful
Deploy to production / deploy (push) Successful in 2m42s
- Changed exercise type from 'pattern_drill' to 'gap_fill' for improved clarity and engagement.
- Revised instructions to specify the use of the verb "adto" across different tenses, enhancing learner guidance.
- Updated question data format to include gaps for each tense, streamlining the exercise layout.
- Adjusted answer data to reflect the new gap-fill structure, ensuring consistency in exercise responses.
- Enhanced explanation to clarify the focus on tense variations with the verb "adto," improving educational value.
2026-04-16 15:06:17 +02:00
Torsten Schulz (local)
a294a94075 feat(vocabLesson): enhance input handling for gap-fill exercises
All checks were successful
Deploy to production / deploy (push) Successful in 2m45s
- Added functionality to manage Enter key events for gap-fill and single input fields, improving user experience during exercises.
- Introduced methods `onGapInputEnter` and `onSingleInputEnter` to streamline answer checking and navigation between input fields.
- Updated input elements with refs for better focus management, ensuring a smoother interaction flow for users.
2026-04-16 14:55:43 +02:00
Torsten Schulz (local)
cb0e1eb2b1 feat(falukant): refactor character ID resolution for political office checks
All checks were successful
Deploy to production / deploy (push) Successful in 2m46s
- Introduced a new method `resolveCharacterIdForOfficeChecks` to robustly resolve character IDs based on user references, improving the reliability of political office queries.
- Updated `getHighestPoliticalOfficeInfo` and `getHighestOfficeAnyInfo` methods to utilize the new character ID resolution logic, enhancing code clarity and reducing redundancy.
- Ensured that character ID retrieval handles both direct and fallback user references, streamlining the process of fetching political office information.
2026-04-15 15:47:31 +02:00
Torsten Schulz (local)
95ea6336b7 feat(falukant): add political office catalog feature and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Implemented the `getPoliticalOfficeCatalog` method in FalukantService to retrieve available political offices and their prerequisites based on user eligibility.
- Updated FalukantController and FalukantRouter to include a new endpoint for accessing the political office catalog.
- Enhanced PoliticsView component to display the list of political offices, including their details and application eligibility.
- Added localization entries for the new offices tab in both German and English, improving user experience and accessibility.
2026-04-15 15:30:24 +02:00
Torsten Schulz (local)
7b4c9a0b1c feat(vocabLesson): add detailed review section for failed chapter exams
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s
- Introduced a new review section that displays questions, user answers, and correct answers for failed chapter exams.
- Added localization entries for the review section in multiple languages, enhancing user experience and understanding of mistakes.
- Implemented logic to format and present failed exam details, improving feedback for learners.
2026-04-15 14:02:34 +02:00
Torsten Schulz (local)
cc791501c9 feat(bisaya-course): enhance lesson content for temporal grammar and exercises
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Updated learning goals to emphasize distinguishing between past, present, and future in everyday sentences.
- Revised core patterns to include specific examples for each tense, improving clarity and context for learners.
- Expanded grammar focus sections to provide detailed explanations of time markers and their usage.
- Introduced new exercises for practicing grammar focus, including multiple-choice questions and transformation tasks, enhancing student engagement with temporal aspects of the language.
- Updated speaking prompts and practical tasks to reflect the new content structure, ensuring a comprehensive learning experience.
2026-04-15 13:25:15 +02:00
Torsten Schulz (local)
feda7d90f2 feat(bisaya-course): enhance grammar focus and drills for lesson content
All checks were successful
Deploy to production / deploy (push) Successful in 2m57s
- Added functions to normalize and build grammar curriculum focus entries, improving the structure and clarity of grammar lessons.
- Implemented logic to merge existing grammar focus with newly defined curriculum focus, ensuring comprehensive coverage of key grammar points.
- Introduced new drills for practicing past and future tense constructions, enhancing student engagement and understanding of temporal aspects in Bisaya.
- Updated the `createBisayaCourseContent` function to incorporate these enhancements, ensuring lessons are enriched with relevant grammar exercises.
2026-04-15 11:52:53 +02:00
Torsten Schulz (local)
ff4fbbfab1 feat(bisaya-course): update exercise content for market-related vocabulary
All checks were successful
Deploy to production / deploy (push) Successful in 2m55s
- Changed the exercise question and answer to focus on market-related scenarios, replacing previous church-related content.
- Updated the explanation to align with the new vocabulary introduced in this lesson, enhancing contextual learning for students.
2026-04-15 11:49:50 +02:00
Torsten Schulz (local)
5d16021557 feat(bisaya-course): implement core pattern extraction and merging for exercises
All checks were successful
Deploy to production / deploy (push) Successful in 2m55s
- Added functions to derive core patterns from various exercise types, including gap-fill, transformation, and multiple-choice.
- Implemented logic to merge derived core patterns with existing lesson patterns, ensuring a minimum count for effective lesson content.
- Enhanced the `createBisayaCourseContent` function to update lessons with merged core patterns and provide detailed logging based on the VOCAB_STRICT_AUDIT environment variable.
2026-04-15 11:12:18 +02:00
Torsten Schulz (local)
3500940d1c feat(bisaya-course): add new lessons for everyday conversations
All checks were successful
Deploy to production / deploy (push) Successful in 3m1s
- Introduced two new lessons: 'Alltagsgespräche - Teil 1' and 'Alltagsgespräche - Teil 2'.
- Each lesson includes learning goals, core patterns, speaking prompts, and practical tasks to enhance conversational skills in everyday contexts.
- Updated the lesson structure to support these additions, ensuring a comprehensive learning experience.
2026-04-15 11:09:14 +02:00
Torsten Schulz (local)
f841a35501 feat(bisaya-course): enhance gap-fill exercise processing and validation
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
- Added `buildCorePatternGlossLookup` to create a mapping of core patterns for improved hint sanitization.
- Implemented `sanitizeGapFillHintText` to validate and replace hints based on answer word count and gloss availability, generating fixes and warnings as needed.
- Updated `sanitizeExerciseForConsistency` to incorporate new sanitization logic for gap-fill exercises, ensuring consistent exercise data handling.
- Enhanced logging in `createBisayaCourseContent` to provide detailed feedback on exercise processing and validation outcomes.
2026-04-14 15:33:00 +02:00
Torsten Schulz (local)
78da376c5b feat(bisaya-course): add word count validation and exercise audit warnings
Some checks failed
Deploy to production / deploy (push) Has been cancelled
- Introduced a `countWords` function to calculate the number of words in a given text.
- Added `collectExerciseAuditWarnings` function to generate warnings for gap-fill exercises with potentially misleading hints.
- Updated the `createBisayaCourseContent` function to log audit warnings when the VOCAB_STRICT_AUDIT environment variable is set, enhancing the validation process for exercise content.
2026-04-14 15:30:58 +02:00
Torsten Schulz (local)
7e5a46a9bf fix(vocabLesson): improve answer validation logic for gap fill exercises
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Enhanced the answer validation process by introducing checks for native word and answer text, ensuring they are trimmed and compared correctly.
- Added logic to identify likely fragment mismatches based on sentence structure and word count, preventing incorrect answer submissions.
- Updated debug logging to provide clearer insights into the validation process, improving maintainability and debugging capabilities.
2026-04-14 14:57:54 +02:00
Torsten Schulz (local)
deb6f5f36c feat(falukant): add improve lover affection feature and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m49s
- Introduced the `improveLoverAffection` method in FalukantService to enhance relationship dynamics by allowing users to boost affection at a cost.
- Updated FalukantController and FalukantRouter to include the new endpoint for improving lover affection.
- Enhanced FamilyView component to provide a button for users to trigger the affection improvement action.
- Added localization entries for the new feature in multiple languages, ensuring clarity in user interactions regarding affection improvements.
2026-04-14 11:39:42 +02:00
Torsten Schulz (local)
26daf5fed5 feat(falukant): enhance notification handling and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Updated the `enrichNotificationsWithCharacterNames` method in FalukantService to include region name enrichment and handle additional character IDs.
- Introduced a new `serializeNotificationForClient` function to format notifications for the client, ensuring all relevant data is included.
- Enhanced the MessagesDialog component to merge notification payloads and extract parameters more effectively, improving the clarity of displayed messages.
- Added new localization entries for director resignation risk and regional festival effects in multiple languages, ensuring comprehensive user notifications.
2026-04-14 08:06:56 +02:00
Torsten Schulz (local)
9deda3147e feat(falukant): add transport cost option to product price calculations
All checks were successful
Deploy to production / deploy (push) Successful in 2m49s
- Updated the `getProductPricesInCitiesBatch` method in FalukantService to accept an options parameter for including transport costs in price calculations.
- Modified the tax calculation logic to conditionally apply transport costs based on the new parameter, enhancing the accuracy of pricing based on regional differences.
- Adjusted the FalukantController to pass the includeTransportCosts option from the request body, improving flexibility in pricing queries.
2026-04-13 16:11:53 +02:00
Torsten Schulz (local)
b50d2a9a93 feat(falukant): refine tax calculation logic to handle political office exemptions
All checks were successful
Deploy to production / deploy (push) Successful in 2m41s
- Updated the tax calculation in FalukantService to ensure no exemptions are applied for political offices, streamlining the cumulative tax query.
- Enhanced the SQL query to accurately compute cumulative tax while excluding exempted ancestor levels based on office, improving overall accuracy in tax assessments.
2026-04-13 16:05:41 +02:00
Torsten Schulz (local)
f92b62e55b feat(falukant): enhance tax calculation and update messaging parameters
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Updated the tax calculation logic in FalukantService to include character-based checks for political offices, improving accuracy in tax assessments.
- Enhanced the MessagesDialog component to incorporate additional parameters such as satisfaction, threshold_percent, director_id, and director_character_id, providing more detailed notifications.
- Added new localization entries for director resignation risk notifications in multiple languages, ensuring users receive clear and contextual information regarding director dynamics.
2026-04-13 15:58:30 +02:00
Torsten Schulz (local)
86e14a875d feat(falukant): add scandalExtraDailyPct field and update related components
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
- Introduced a new field `scandalExtraDailyPct` in the relationship state model to track additional scandal risk per day, with validation constraints.
- Updated the FalukantService to include the new field in relevant calculations and data handling.
- Enhanced the frontend components, including RevenueSection and FamilyView, to display the scandal risk information and updated price calculations.
- Added localization entries for the new field in multiple languages to ensure clarity for users.
2026-04-13 15:31:47 +02:00
Torsten Schulz (local)
b0624422b8 feat(falukant): update production counting logic and enhance localization
All checks were successful
Deploy to production / deploy (push) Successful in 2m55s
- Modified the production counting logic in FalukantService to count each completed production directly from the falukant_log.production table, simplifying the query structure.
- Added a new localization entry for "scoreHowToRaise" in Cebuano, German, English, Spanish, and French to provide users with clear guidance on how to improve their score through various factors.
- Updated the OverviewView component to display the new score improvement information, enhancing user experience and understanding of scoring dynamics.
2026-04-13 13:59:37 +02:00
Torsten Schulz (local)
162e908c1c feat(falukant): add 'lover_breakup_risk_high' event handling and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Introduced a new event type for high breakup risk in relationships, including a sample payload for integration.
- Updated the MessagesDialog component to handle new parameters related to breakup risk.
- Added translations for the new event in Cebuano, German, English, Spanish, and French to enhance user understanding of relationship dynamics.
- Enhanced documentation to reflect the new notification structure for high breakup risk events.
2026-04-13 12:21:24 +02:00
Torsten Schulz (local)
1bce855b3a feat(falukant): enhance marriage actions with production delay handling and UI updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m58s
- Implemented logic in FalukantService to delay running productions when spending time with a spouse, updating the start timestamp accordingly.
- Enhanced the response from the spend time API to include details about delayed productions and household order changes.
- Updated FamilyView component to display the cost of marriage actions and added a help section detailing the effects of actions, including production delays.
- Added new translations for action effects in multiple languages to improve user understanding of the marriage actions.
2026-04-13 12:09:17 +02:00
Torsten Schulz (local)
ba72d4fb74 feat(i18n): update 'acknowledgeHelp' translations and enhance UI interaction
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Added new translations for 'maintenance', 'affection', and 'breakup' in Cebuano, German, English, and Spanish to provide users with comprehensive context regarding relationship dynamics.
- Refactored the FamilyView component to utilize a details element for the 'acknowledgeHelp' section, improving user interaction and readability.
- Enhanced styling for the summary element to improve visibility and user engagement.
2026-04-13 11:41:17 +02:00
Torsten Schulz (local)
e2f4b255ff removed double question
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
2026-04-13 11:27:06 +02:00
Torsten Schulz (local)
60ef98283f fix(falukant): validate office term length and adjust term end calculation
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Added validation for office term length to ensure it is a positive finite number, throwing an error for invalid values.
- Updated the calculation of the term end date to use the term length in days instead of years, aligning with the expected data format.
2026-04-10 16:08:50 +02:00
Torsten Schulz (local)
c6419c6c34 feat(vocab): implement pagination and localization for vocabulary dictionaries
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
- Added pagination functionality to the vocabulary dictionary views, allowing users to navigate through results efficiently.
- Introduced a new method `_parseDictionaryPaging` in `VocabService` to handle pagination parameters.
- Updated `getLanguageDictionary` and `getCourseDictionary` methods to return pagination details alongside results.
- Enhanced the `VocabDictionaryView` component with pagination controls and updated UI for better user experience.
- Added localization entries for pagination in Cebuano, German, English, Spanish, and French, ensuring a consistent user experience across languages.
2026-04-10 14:35:50 +02:00
Torsten Schulz (local)
f46c864bbc feat(vocab-lesson): implement pagination for vocabulary overview in lesson view
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
- Added pagination functionality to the vocabulary overview in the VocabLessonView component, allowing users to navigate through vocabulary items more efficiently.
- Introduced new localization entries for pagination controls in Cebuano, German, English, Spanish, and French, ensuring a consistent user experience across languages.
- Enhanced the UI with buttons for previous and next navigation, improving accessibility and usability of the vocabulary list.
2026-04-10 14:26:01 +02:00
Torsten Schulz (local)
545314e905 feat(i18n, frontend): enhance course planning with optional steps and localization updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Added a new section for optional learning steps in the course planning UI, allowing users to engage with additional content when no mandatory tasks are due.
- Updated localization files for Cebuano, German, English, Spanish, and French to reflect changes in course planning instructions and titles, ensuring clarity and consistency across languages.
- Improved pedagogical logic for lesson recommendations, focusing on cognitive load and spaced repetition principles to enhance user learning experience.
2026-04-10 13:33:25 +02:00
Torsten Schulz (local)
d17c8a341d feat(vocab): add language and course dictionary endpoints and UI components
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s
- Implemented `getLanguageDictionary` and `getCourseDictionary` methods in the VocabService to retrieve vocabulary entries filtered by search terms.
- Updated VocabController and vocabRouter to include new routes for accessing language and course dictionaries.
- Enhanced frontend components to navigate to the new dictionary views, including buttons in VocabCourseView and VocabLanguageView.
- Added localization entries for the dictionary feature in multiple languages, ensuring a consistent user experience across the platform.
2026-04-10 13:08:18 +02:00
Torsten Schulz (local)
9582e7b900 feat(i18n, frontend): add 'acknowledgeHelp' translations and UI enhancements
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Introduced 'acknowledgeHelp' translations in Cebuano, German, English, and Spanish to provide users with context about the implications of acknowledging relationships.
- Updated the FamilyView component to display the new 'acknowledgeHelp' information, enhancing user understanding of relationship dynamics.
- Improved styling for the acknowledgment help section to ensure clarity and visibility within the user interface.
2026-04-10 10:29:20 +02:00
Torsten Schulz (local)
7e0821c96b feat(falukant): enhance reputation handling in character checks
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Added the 'reputation' attribute to the character query in FalukantService, improving data retrieval for nobility requirements.
- Refactored the `checkReputationRequirement` method to streamline reputation checks, ensuring it handles undefined or null values more effectively, enhancing reliability in user character evaluations.
2026-04-10 08:45:54 +02:00
Torsten Schulz (local)
3450bac99c feat(falukant): enhance notification handling and parameter extraction in MessagesDialog
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Introduced a new method `mergeNotificationPayload` to consolidate top-level fields with embedded JSON from notifications, improving data handling.
- Updated `formatBody` and `extractParams` methods to utilize the merged payload, ensuring accurate extraction of parameters from notifications.
- Enhanced parameter extraction logic to handle various notification structures, improving clarity and consistency in displayed information.
2026-04-10 08:41:53 +02:00
Torsten Schulz (local)
1dfa49191f feat(vocab-lesson): enhance exercise navigation and randomization logic
All checks were successful
Deploy to production / deploy (push) Successful in 2m57s
- Improved the exercise progression logic to automatically advance only on correct answers, ensuring better user feedback during learning.
- Added a new method to shuffle multiple-choice options display order when distractor replacement is not applicable, enhancing the exercise experience.
- Updated the build process for randomized options to include both randomization from a pool and display order shuffling, improving overall engagement and learning outcomes.
2026-04-09 17:57:51 +02:00
Torsten Schulz (local)
5cbd6d06b1 feat(bisaya-course): refine exercises and instructions for improved learning experience
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
- Updated exercise titles and instructions to enhance clarity and focus on specific learning objectives.
- Modified question data and answer options to better align with the lesson's vocabulary and context.
- Improved explanations to provide clearer guidance on language usage and learning goals, fostering better comprehension for learners.
2026-04-09 17:49:50 +02:00
Torsten Schulz (local)
2b66f76dd9 feat(bisaya-course): add new exercises to enhance Bisaya language learning
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Introduced multiple-choice and transformation exercises focused on common phrases and vocabulary, including translations and contextual usage.
- Expanded the curriculum with practical tasks that encourage conversational skills and comprehension in various scenarios.
- Enhanced existing exercises with clear instructions and explanations to improve user engagement and learning outcomes.
2026-04-09 17:23:20 +02:00
Torsten Schulz (local)
204b7aed04 feat(bisaya-course): expand 'Haus & Familie' lesson with new vocabulary and exercises
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Added comprehensive learning goals, core patterns, grammar focus, speaking prompts, and practical tasks for the 'Haus & Familie' lesson, enhancing the curriculum for better language acquisition.
- Updated exercise content to include multiple-choice questions and gap-fill tasks related to household vocabulary, improving user engagement and learning outcomes.
- Enhanced localization for the lesson description and vocabulary, ensuring clarity and consistency across the learning platform.
2026-04-09 17:19:02 +02:00
Torsten Schulz (local)
f5a5639e97 feat(seo, i18n): update SEO metadata and enhance localization across multiple languages
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s
- Revised SEO titles and descriptions for improved clarity and relevance, emphasizing the platform's features such as community, chat, forums, and browser games.
- Updated localization files for Cebuano, German, English, Spanish, and French to include new translations and enhance existing content, ensuring a consistent user experience across languages.
- Added a new story teaser in the home view to better highlight the platform's offerings without requiring downloads.
2026-04-09 09:44:53 +02:00
Torsten Schulz (local)
641a2134cb fix(falukant): improve product pricing logic and update localization for office notifications
All checks were successful
Deploy to production / deploy (push) Successful in 2m44s
- Refactored the product pricing logic in `falukantService.js` to ensure accurate regional pricing calculations based on client values and server data.
- Added new notification translations for "office filled" in multiple languages (Cebuano, German, English, Spanish, French) to enhance user experience and clarity in notifications.
2026-04-09 09:21:47 +02:00
Torsten Schulz (local)
1118a691b9 feat(falukant): enhance product pricing and nobility advancement features
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Updated the `getAllProductPricesInRegion` method in `FalukantService` to accept additional parameters for network worth and branch ID, improving pricing calculations based on user branches.
- Enhanced the nobility advancement logic in `NobilityView` to display unmet requirements clearly, providing users with better feedback on advancement conditions.
- Refactored the revenue calculation in `ProductionSection` to utilize a cached product prices object, optimizing performance and reducing redundant API calls.
- Updated localization files to include new translations for attack sources across multiple languages, enhancing the user experience for diverse audiences.
- Removed obsolete C++ worker references and streamlined the retention logic for production logs, ensuring efficient data management.
2026-04-09 09:08:32 +02:00
Torsten Schulz (local)
360bb59a4e feat(user): add certificate production tracking and update localization
All checks were successful
Deploy to production / deploy (push) Successful in 2m50s
- Introduced a new field `certificateProductionsCountSince` in the `FalukantUser` model to track the date from which production logs are counted for certificate requirements.
- Updated the `FalukantService` to utilize the new field for calculating completed productions since the specified date.
- Enhanced the UI to display the count of productions since the last promotion, with corresponding translations added for multiple languages including Cebuano, German, English, Spanish, and French.
- Implemented a method to delete old production logs, ensuring efficient data management while maintaining necessary historical records for certificate calculations.
2026-04-09 08:19:19 +02:00
Torsten Schulz (local)
f7030bbabe feat(i18n): add French language support and enhance localization
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Introduced French as a supported language across the application, updating locale files and adding translations for various components.
- Enhanced language handling logic to accommodate French, ensuring proper detection and fallback mechanisms.
- Updated UI elements to include French language options, improving accessibility for French-speaking users.
- Refactored SEO handling to include French in hreflang links, enhancing search engine indexing for multilingual content.
- Added new scripts for managing French translations and ensuring consistency across language files.
2026-04-07 18:04:03 +02:00
Torsten Schulz (local)
f715c6125d feat(i18n): implement deep merging for locale chunks and enhance Cebuano translations
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Introduced a deep merge function to combine locale chunks, improving the handling of language data for Cebuano.
- Updated Cebuano locale files with comprehensive translations, including new sections for admin, social network, and settings.
- Enhanced existing translations for clarity and consistency across various components, ensuring a better user experience.
- Added new fields in the settings and profile sections to capture more user attributes, improving personalization options.
2026-04-07 16:23:11 +02:00
Torsten Schulz (local)
e9f0f6c133 fix(i18n): update Cebuano language label in AppHeader.vue
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s
- Changed the native label for Cebuano from 'Sinugboanon' to 'Binisaya' in the language options of AppHeader.vue for improved accuracy and user recognition.
2026-04-07 15:59:07 +02:00
Torsten Schulz (local)
ae635b9c16 refactor(i18n): improve language handling and UI updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m49s
- Enhanced language setting logic in App.vue and AppHeader.vue to support reactive updates without page reloads.
- Updated language options in AppHeader.vue to use native labels for improved clarity.
- Introduced a utility function in i18n/index.js to streamline locale updates, ensuring consistent language handling across the application.
2026-04-07 15:52:16 +02:00
Torsten Schulz (local)
c5b8860605 feat(seo): enhance multilingual support and SEO handling
All checks were successful
Deploy to production / deploy (push) Successful in 2m46s
- Added support for multiple languages in the frontend, including English, Spanish, and Cebuano, improving accessibility for a broader audience.
- Implemented hreflang links for better SEO performance, ensuring search engines can correctly index language-specific content.
- Updated SEO metadata handling to utilize internationalization keys, enhancing the clarity and relevance of page titles and descriptions.
- Refactored SEO utility functions to streamline the management of OpenGraph and hreflang attributes, improving maintainability and performance.
2026-04-07 15:43:16 +02:00
Torsten Schulz (local)
ebb2283646 refactor(exercises): standardize answer language handling across exercise scripts
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Introduced a mechanism to infer answer language based on question phrasing in multiple exercise scripts, enhancing consistency in exercise data.
- Updated question formats to clarify the intent of exercises, improving user understanding and engagement.
- Streamlined the code for better maintainability and clarity in exercise generation processes.
2026-04-07 14:32:44 +02:00
Torsten Schulz (local)
160c9dafb2 refactor(vocab): enhance token weight handling in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m45s
- Introduced a new mechanism for calculating token weights based on core patterns and important vocabulary, improving the accuracy of target-gloss pair orientation.
- Replaced the previous hint logic with a more robust token-based scoring system, enhancing the quality of vocabulary item representation.
- Streamlined the code for better maintainability and clarity in vocabulary preparation processes.
2026-04-07 11:32:14 +02:00
Torsten Schulz (local)
ee338c0e49 refactor(vocab): update vocabulary label handling in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m44s
- Replaced computed properties for current vocabulary preparation labels with direct references to new properties, enhancing clarity and maintainability.
- Improved the logic for populating native and target hints, ensuring accurate representation of vocabulary items.
- Removed outdated label swapping logic, streamlining the code and focusing on course-specific language names for better user experience.
2026-04-07 11:22:22 +02:00
Torsten Schulz (local)
fe9322c098 feat(vocab): attach language names to course data in VocabService
All checks were successful
Deploy to production / deploy (push) Successful in 2m49s
- Added a method to attach language names to course rows, enhancing the clarity and localization of vocabulary lessons.
- This update improves the overall user experience by ensuring accurate representation of language names in course data.
2026-04-07 11:13:38 +02:00
Torsten Schulz (local)
3a1d83f20c feat(vocab): add course language name handling in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Introduced new properties for course language names to enhance clarity in vocabulary preparation.
- Implemented a method to load course language names from the API, improving localization and user experience.
- Updated label logic to utilize course-specific language names, ensuring accurate representation in vocabulary lessons.
2026-04-07 10:29:28 +02:00
Torsten Schulz (local)
e649236e39 feat(vocab): refine vocabulary label logic in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Introduced computed properties for current vocabulary preparation labels, enhancing clarity and localization.
- Updated label handling to dynamically adjust based on the context of target and gloss items, improving user experience.
- Enhanced the logic for determining when to swap labels, ensuring accurate representation of vocabulary items for better learning outcomes.
2026-04-07 10:22:36 +02:00
Torsten Schulz (local)
8f6f06caf0 feat(vocab): enhance vocabulary label handling in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m50s
- Refactored VocabLessonView to utilize computed properties for vocabulary preparation labels, improving localization and maintainability.
- Updated the logic for target and gloss labels to prioritize course-specific language names, enhancing user experience and clarity in vocabulary presentation.
- Improved the orientation logic for target-gloss pairs to ensure accurate representation of vocabulary items, contributing to better learning outcomes.
2026-04-07 10:07:54 +02:00
Torsten Schulz (local)
62503191d4 feat(vocab): improve vocabulary preparation logic in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m44s
- Refactored prepItems method to ensure only valid target-gloss pairs are included, enhancing the quality of vocabulary preparation.
- Introduced a mechanism to limit the output to a maximum of 30 unique pairs, improving the compactness of the preparation round.
- Enhanced handling of core patterns and extracted vocabulary to prioritize high-quality learning materials.
2026-04-07 09:58:33 +02:00
Torsten Schulz (local)
86dfb0d859 feat(vocab): update course today plan localization and UI logic
All checks were successful
Deploy to production / deploy (push) Successful in 2m49s
- Added new localization strings for scenarios with no due reviews in German, English, and Spanish, enhancing user guidance.
- Updated VocabCourseView to conditionally display the appropriate introduction message based on the presence of due reviews, improving clarity in the course flow.
2026-04-07 09:55:01 +02:00
Torsten Schulz (local)
f504a5d597 feat(vocab): enhance feedback acknowledgment and localization in vocabulary lessons
All checks were successful
Deploy to production / deploy (push) Successful in 2m41s
- Added acknowledgment messages for exercise reinforcement in German, English, and Spanish localization files to improve user guidance.
- Updated VocabLessonView to include a feedback acknowledgment button, enhancing user interaction after answering questions.
- Implemented logic to track feedback acknowledgment state, improving the flow of lesson reviews and user experience.
2026-04-07 09:47:45 +02:00
Torsten Schulz (local)
3cc5f63610 feat(vocab): add lesson review functionality and update navigation
All checks were successful
Deploy to production / deploy (push) Successful in 2m46s
- Introduced VocabLessonReviewView for reviewing lessons within the vocabulary course structure.
- Updated routing in socialRoutes.js to include a new path for lesson reviews, ensuring authenticated access.
- Modified VocabCourseView to change the lesson button click behavior to navigate to the review view, enhancing user flow.
- Added a new method to handle lesson review navigation, improving the overall user experience in the vocabulary section.
2026-04-07 09:38:00 +02:00
Torsten Schulz (local)
e17f0cdce0 feat(vocab): enhance vocabulary exercises and localization support
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Updated core patterns in BISAYA_PHASE5_DIDACTICS to include gloss translations for better understanding.
- Refactored vocabulary exercise generation in update-food-care-exercises.js to improve randomization and user engagement.
- Added new exercise types and improved question structures for vocabulary lessons, enhancing the learning experience.
- Enhanced localization files for German, English, and Spanish to support new exercise features and improve user guidance.
- Updated VocabLessonView to incorporate sequential navigation for exercises, providing a more structured learning flow.
2026-04-07 09:09:43 +02:00
Torsten Schulz (local)
d192bcae2d refactor(vocabService, VocabLessonView): unify answer language classification for multiple-choice questions
All checks were successful
Deploy to production / deploy (push) Successful in 2m51s
- Renamed and refactored the method for classifying answer languages in multiple-choice questions from _classifyMcQuestionSide to _resolveMcAnswerSide for clarity and consistency.
- Updated the logic to classify answer languages based on questionData.answerLanguage and answerLanguageId, improving accuracy in determining the correct language context.
- Adjusted VocabLessonView to utilize the new classification method, ensuring alignment with backend changes and enhancing the handling of distractor options based on answer language.
- Enhanced documentation to clarify the expected input and output for the new method, improving maintainability and understanding of the codebase.
2026-04-07 08:34:19 +02:00
Torsten Schulz (local)
07ab648143 feat(political-office): enhance political office benefits and salary computation
All checks were successful
Deploy to production / deploy (push) Successful in 3m6s
- Added a new hierarchyLevel field to PoliticalOfficeType for better categorization of political roles.
- Updated computePoliticalDailySalaryPayout function to incorporate hierarchy level in salary calculations, allowing for more dynamic salary adjustments based on office rank.
- Modified SQL scripts to reflect changes in political office benefits, ensuring compatibility with the new salary structure.
- Enhanced localization files to support updated benefit descriptions and salary formats across multiple languages.
- Improved UI components to display the new salary calculations and benefits accurately in the PoliticsView.
2026-04-02 16:49:18 +02:00
Torsten Schulz (local)
e063df5cbe refactor(PoliticsView): enhance UI layout and styling for political information display
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Improved the layout of the political information section by introducing a structured row format for better readability.
- Updated CSS styles to enhance the visual hierarchy and alignment of labels and values, ensuring a more user-friendly interface.
- Adjusted the display of benefits and term end information to provide clearer context and organization within the PoliticsView component.
2026-04-02 16:09:52 +02:00
Torsten Schulz (local)
56be4b76c0 feat(political-benefits): implement political powers and benefits system
All checks were successful
Deploy to production / deploy (push) Successful in 3m3s
- Added new political powers and benefits functionalities, including reputation ticks, tax jurisdiction management, and appointment capabilities.
- Introduced a new job for periodic reputation updates and created necessary database tables for tracking political benefits.
- Enhanced the FalukantController and services to support new endpoints for managing political powers and appointments.
- Updated localization files to reflect new features and improve user experience across multiple languages.
- Modified the UI to display new political powers and benefits, ensuring accurate representation in the PoliticsView.
2026-04-02 16:00:29 +02:00
Torsten Schulz (local)
5d06d97737 feat(politics): add new political office benefits and enhance database migration
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
- Introduced new benefits including 'reputation_periodic', 'appoint_politicians', 'set_regional_tax', 'free_lover_slots', 'guard_protection', and 'court_immunity' to the political office system.
- Updated database migration to add and remove the 'last_political_daily_salary_on' column using SQL queries for better performance.
- Enhanced localization files for multiple languages to support new benefits, improving user experience across the application.
- Updated UI components to display new benefits correctly in the PoliticsView, ensuring accurate representation of political office functionalities.
2026-04-02 15:27:05 +02:00
Torsten Schulz (local)
49713d957d feat(dashboard): enhance widget availability and initialization
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
- Updated `getAvailableWidgets` method in `DashboardService` to merge default widget types with database entries, ensuring immediate visibility of new widgets post-deployment.
- Introduced `DASHBOARD_WIDGET_TYPE_DEFAULTS` in `initializeWidgetTypes` for canonical widget types, facilitating API merging when database entries are absent.
- Modified `StatusBar.vue` to utilize a computed property for quick access children, improving menu item handling and visibility based on user context.
- Enhanced `LoggedInView.vue` to dynamically return localized widget labels, improving user experience with accurate translations.
2026-04-02 15:19:08 +02:00
Torsten Schulz (local)
5fcd55be43 feat(vocab): add dashboard learning summary and related endpoints
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Introduced `getDashboardLearningSummary` method in `VocabService` to provide a compact overview of enrolled courses and current lessons for users.
- Updated `vocabController` to include a new route for the dashboard widget, allowing users to access their learning summary.
- Enhanced `vocabRouter` to route requests for the new dashboard widget endpoint.
- Added localization support for the new dashboard features across multiple languages, improving user engagement and accessibility.
- Updated UI components to integrate the new dashboard widget, ensuring a seamless user experience.
2026-04-02 15:06:50 +02:00
Torsten Schulz (local)
77e6f8d3e8 feat(vocab): enhance vocabService and VocabLessonView for improved exercise handling
All checks were successful
Deploy to production / deploy (push) Successful in 2m58s
- Updated VocabService to include validation for synthetic exercise IDs and added methods for generating multiple-choice exercises based on chapter lexemes.
- Implemented a seeded shuffle function to randomize distractor options in multiple-choice questions, ensuring varied user experiences.
- Modified VocabLessonView to adjust target calculations for lesson goals and unlock attempts, increasing the maximum limits for better flexibility in user progress tracking.
2026-04-02 14:23:21 +02:00
Torsten Schulz (local)
3d2ccd620a feat(localization): enhance course progress and review messaging across multiple languages
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- Added new confirmation titles and messages for resetting and marking lessons as complete in admin and user interfaces.
- Expanded course flow and review scheduling messages to improve clarity and user guidance in Cebuano, German, Spanish, and English.
- Introduced a new section in the VocabCourseView to display today's recommended steps for users, enhancing the learning experience.
- Updated localization files to ensure consistent messaging and improved user engagement across all supported languages.
2026-04-02 13:49:59 +02:00
Torsten Schulz (local)
edbf22ac5b extended admin tool for finished lessons
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
2026-04-02 13:32:13 +02:00
Torsten Schulz (local)
9d663e4f2b feat(vocab): enhance lesson progress tracking and review scheduling
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Updated VocabService to include lessonId and lessonNumber in progress data, improving tracking accuracy.
- Modified getLessonProgress and lastProgressTouch methods to accept lesson parameters, enhancing flexibility in progress retrieval.
- Implemented formatReviewBadgeSchedule method to manage review scheduling notifications, providing clearer user feedback.
- Updated VocabCourseView to reflect changes in lesson progress handling, ensuring accurate display of review statuses and due dates.
- Expanded localization for review scheduling messages across multiple languages, enhancing user experience.
2026-04-02 13:27:24 +02:00
Torsten Schulz (local)
153914d5d2 feat(bisaya-course): add family vocabulary and exercises for relatives
All checks were successful
Deploy to production / deploy (push) Successful in 2m57s
- Introduced new content for the 'Familie - Verwandte & Stieffamilie' lesson, including learning goals, core patterns, grammar focus, speaking prompts, and practical tasks.
- Added multiple-choice and gap-fill exercises related to family vocabulary, enhancing the learning experience for users.
- Updated course content to reflect the new lesson and exercises, ensuring comprehensive coverage of family-related terms in Bisaya.
2026-04-02 09:31:46 +02:00
Torsten Schulz (local)
2272db7f91 feat(admin): add user vocab course management functionality
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
- Implemented `getUserVocabCourses` and `getVocabCourseForAdmin` methods in `AdminController` to allow admins to retrieve enrolled vocab courses for users and specific course details, respectively.
- Updated `adminRouter` to include new routes for accessing user vocab courses and course details.
- Enhanced `AdminService` with methods to list user-enrolled vocab courses and retrieve course information with lessons, ensuring proper access control.
- Improved `VocabService` to support the new functionalities, including attaching language names to course data.
- Updated UI components in `UsersView` to reflect changes, including error handling and loading states for course retrieval, along with localization updates for new features.
2026-04-02 09:21:52 +02:00
Torsten Schulz (local)
b3c8e8e210 refactor(ui): enhance AppContent layout for improved routing and styling
All checks were successful
Deploy to production / deploy (push) Successful in 3m0s
- Wrapped the router-view in a new div to manage layout and styling more effectively.
- Updated CSS to ensure proper height management and scrolling behavior, allowing for better content display.
- Added comments to clarify the purpose of layout adjustments and ensure maintainability.
2026-04-02 08:56:24 +02:00
Torsten Schulz (local)
02f3e82987 fix(ui): update styles and localization in UsersView and AppShell
All checks were successful
Deploy to production / deploy (push) Successful in 3m1s
- Added styles to ensure the app shell content occupies only the remaining space, preventing overflow issues.
- Updated the header in UsersView to reflect accurate localization for user administration.
2026-04-02 08:48:06 +02:00
Torsten Schulz (local)
c3b2c60362 feat(vocab): implement user vocab lesson progress reset functionality
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
- Added `resetUserVocabLessonProgress` method in `AdminController` to allow admins to reset a user's progress for a specific vocab lesson.
- Introduced corresponding route in `adminRouter` for the new reset functionality.
- Enhanced `VocabService` with methods to purge lesson progress for users, ensuring that only the specified lesson's progress is affected.
- Updated UI components in `UsersView` to facilitate the selection of courses and lessons for resetting progress, including confirmation dialogs and loading states.
- Added localization support for the new reset functionality across multiple languages.
- Implemented reset functionality in `VocabLessonView` for users to reset their own lesson progress.
2026-04-02 08:25:56 +02:00
Torsten Schulz (local)
13534498fa feat(user): enhance email handling with validation and normalization functions
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Introduced `looksLikePlausibleEmail` to validate email format, ensuring only plausible addresses are processed.
- Added `normalizeEmailCandidate` to standardize email input, returning null for invalid formats.
- Updated `decodeEncryptedBlob` to utilize normalization functions for both UTF-8 and hex formats, improving email decryption reliability.
- Adjusted `SettingsService` to ensure email is set after merging adult access state, maintaining data integrity.
2026-04-02 08:07:25 +02:00
Torsten Schulz (local)
3fb4fb92c6 refactor(ui): update layout styles for AppContent and AccountView components
Some checks failed
Deploy to production / deploy (push) Has been cancelled
- Changed layout from grid to flex in AccountView for improved responsiveness and alignment.
- Adjusted flex properties in AppContent to ensure proper scrolling behavior and height management.
- Enhanced styling for account settings panel to support better content overflow handling and visual consistency.
2026-04-02 08:05:32 +02:00
Torsten Schulz (local)
6d9d69dc10 feat(localization): expand language support and enhance UI for user settings
All checks were successful
Deploy to production / deploy (push) Successful in 3m0s
- Added support for additional UI locales including Cebuano and Spanish, improving accessibility for a broader user base.
- Updated language selection components in the AppHeader and SettingsWidget to reflect new language options, enhancing user experience.
- Enhanced localization of various UI elements across components, ensuring consistent language representation and improved user engagement.
- Implemented logic to synchronize user language preferences with backend settings, providing a seamless experience when changing languages.
2026-04-02 07:54:44 +02:00
Torsten Schulz (local)
ac5d436a36 feat(vocab): implement scheduled review management in VocabService and UI updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Added methods in VocabService to handle scheduled review states, including normalization of review dates and management of review stages.
- Enhanced lesson state management to support review scheduling, improving the learning process for users.
- Updated VocabCourseView and VocabLessonView to display review statuses and due dates, providing clearer feedback on lesson progress and review requirements.
- Introduced new UI elements to indicate review status, enhancing user engagement and understanding of lesson timelines.
2026-04-01 16:03:42 +02:00
Torsten Schulz (local)
3ff8e4fc40 refactor(bisaya-course): enhance pedagogy logic for German lessons
All checks were successful
Deploy to production / deploy (push) Successful in 2m57s
- Updated the `getGermanForBisayaLessonPedagogy` function to include lesson titles for improved didactic mode determination, specifically adding support for 'contrast_training'.
- Modified multiple scripts to utilize the updated pedagogy function, ensuring consistent application of the new logic across various course phases.
- Enhanced the VocabService to recognize and handle 'contrast_training' as a didactic mode, improving lesson management and user experience.
- Updated UI components to reflect the new didactic mode, ensuring clarity in lesson presentation.
2026-04-01 15:52:53 +02:00
Torsten Schulz (local)
10fc78e81d feat(falukant): implement score threshold logic and enhance UI feedback for certificate progression
All checks were successful
Deploy to production / deploy (push) Successful in 3m3s
- Added a new function to calculate score thresholds based on certificate levels, improving the logic for determining promotion eligibility.
- Updated the FalukantService to include new properties for score and requirement checks, enhancing the decision-making process for certificate readiness.
- Enhanced the OverviewView component to display detailed hints and states regarding certificate progression, providing users with clearer feedback on their status.
- Localized new strings in multiple languages to support the updated UI elements and hints, improving user experience across different languages.
2026-04-01 15:47:11 +02:00
Torsten Schulz (local)
d39cea2c01 refactor(vocab): unify text normalization across VocabService and VocabPracticeDialog
All checks were successful
Deploy to production / deploy (push) Successful in 3m3s
- Enhanced text normalization methods in VocabService and VocabPracticeDialog to include normalization and improved regex for punctuation and symbols, ensuring consistent text handling.
- This update streamlines the normalization process, contributing to better user input processing and overall application reliability.
2026-04-01 15:24:22 +02:00
Torsten Schulz (local)
d38231b52c refactor(vocab): improve text normalization consistency in VocabService and VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Enhanced text normalization methods in both VocabService and VocabLessonView to include an additional trim operation, ensuring leading and trailing spaces are removed.
- This change improves the consistency and accuracy of text handling across the application, contributing to better user input processing.
2026-04-01 15:09:15 +02:00
Torsten Schulz (local)
7fee9e12d4 refactor(vocab): standardize text normalization methods in VocabService and VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 3m1s
- Updated text normalization logic in VocabService to use a regex that replaces punctuation with spaces, improving text handling consistency.
- Refactored VocabLessonView to utilize the new normalization method for comparable text, enhancing the accuracy of user input processing.
- Ensured both components now share a unified approach to text normalization, streamlining code and improving maintainability.
2026-04-01 14:56:39 +02:00
Torsten Schulz (local)
0421b2bc00 feat(bisaya-course): expand lesson didactics with new topics and detailed grammar focus
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
- Added new lesson topics including 'Überlebenssätze - Teil 1', 'Familien-Gespräche', 'Gefühle & Zuneigung', and 'Überlebenssätze - Teil 2' to enhance the curriculum.
- Updated existing lessons with more detailed learning goals, core patterns, grammar focus, speaking prompts, and practical tasks to improve learner engagement and comprehension.
- Refactored core patterns to include glosses for better understanding of vocabulary context.
- Enhanced the structure of lesson didactics to provide a more comprehensive learning experience for users.
2026-04-01 14:30:31 +02:00
Torsten Schulz (local)
09015b4244 feat(vocab): implement repeat queue management in VocabService and VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m59s
- Added a new method in VocabService to sanitize and manage a repeat queue for vocabulary items, enhancing the learning process.
- Updated VocabLessonView to incorporate repeat queue functionality, allowing for better tracking of vocabulary that needs review.
- Refactored existing logic to ensure seamless integration of repeat queue features, improving user experience during vocabulary lessons.
2026-04-01 14:06:34 +02:00
Torsten Schulz (local)
a3b820cea0 feat(vocab): enhance lesson state management and persistence in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 3m20s
- Added a new JSONB field `lessonState` to the VocabCourseProgress model to store detailed lesson state information.
- Implemented methods in VocabService for sanitizing and serializing lesson state, ensuring robust data handling.
- Updated VocabLessonView to manage lesson state persistence, including local storage and server synchronization, improving user experience during vocabulary lessons.
- Introduced mechanisms for exporting and normalizing exercise answers, enhancing the accuracy of saved progress.
2026-04-01 11:16:56 +02:00
Torsten Schulz (local)
84adfeafb4 feat(vocab): add grammar explanations to VocabLessonView and enhance localization
All checks were successful
Deploy to production / deploy (push) Successful in 2m53s
- Introduced new sections for grammar explanations in VocabLessonView, providing users with contextual insights during vocabulary lessons.
- Added localization keys in German, English, and Spanish for the new grammar explanation titles and introductions, improving user guidance across languages.
- Updated styles for the grammar explanation section to enhance visual clarity and user experience.
2026-04-01 10:22:53 +02:00
Torsten Schulz (local)
b8e3732ef8 feat(i18n, vocab): enhance localization and vocabulary preparation in VocabLessonView
Some checks failed
Deploy to production / deploy (push) Has been cancelled
- Added new localization keys in German, English, and Spanish for exercise flow, progress, and learning path instructions, improving user guidance across languages.
- Updated VocabLessonView to incorporate these new keys, enhancing the clarity of vocabulary preparation steps and overall user experience during lessons.
- Refactored the layout to better present the learning path and vocabulary preparation stages, ensuring a more cohesive and informative interface.
2026-04-01 10:21:13 +02:00
Torsten Schulz (local)
8bbfd46ada feat(i18n): add new localization keys for lesson details and deepening sections
All checks were successful
Deploy to production / deploy (push) Successful in 2m46s
- Introduced new localization strings in German, English, and Spanish for lesson detail toggles and deepening sections, enhancing user experience across multiple languages.
- Updated VocabLessonView to incorporate these new localization keys, improving the clarity and accessibility of lesson information for users.
2026-04-01 10:05:37 +02:00
Torsten Schulz (local)
6d13965c76 feat(vocab): integrate fallback core patterns and enhance vocabulary display in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m44s
- Added a new method in VocabService to merge core pattern glosses with fallback patterns, improving vocabulary clarity and consistency.
- Updated VocabLessonView to utilize the merged core patterns, ensuring a comprehensive vocabulary overview for users.
- Refactored vocabulary handling logic to enhance user experience during vocabulary lessons, including improved display of lesson vocabulary.
2026-04-01 09:58:32 +02:00
Torsten Schulz (local)
1328e4983e feat(vocab): enrich core patterns with glosses in VocabService and VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m54s
- Implemented a new method in VocabService to enhance core patterns with glosses derived from extracted vocabulary, improving the clarity of vocabulary entries.
- Updated the VocabLessonView to utilize the enriched core patterns, ensuring that glosses are displayed alongside vocabulary targets for better user comprehension.
- Refactored vocabulary preparation logic to integrate glosses seamlessly, enhancing the overall user experience during vocabulary lessons.
2026-04-01 09:40:33 +02:00
Torsten Schulz (local)
02b3636e10 feat(vocab-prep): implement enhanced vocabulary preparation steps in VocabLessonView
All checks were successful
Deploy to production / deploy (push) Successful in 2m58s
- Introduced a structured vocabulary preparation process with two review stages before engaging with the vocabulary trainer.
- Added localization support for new vocabulary preparation messages in German, English, and Spanish, improving accessibility for users.
- Updated the VocabLessonView component to display current progress and next steps during vocabulary preparation, enhancing user guidance.
- Refactored related logic to manage preparation stages and item navigation effectively.
2026-04-01 08:58:36 +02:00
Torsten Schulz (local)
0c89c48e68 feat(bisaya-course): restructure core patterns and enhance vocabulary preparation
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Updated core patterns in various scripts to use an object format with target phrases and glosses, improving clarity and usability for learners.
- Enhanced the VocabService to normalize core pattern entries, ensuring consistent handling of vocabulary data.
- Introduced new vocabulary preparation steps in the VocabLessonView, guiding users through active review processes before engaging with the vocabulary trainer.
- Added localization support for new vocabulary preparation hints and instructions in multiple languages, enhancing user experience across the application.
2026-04-01 08:12:57 +02:00
Torsten Schulz (local)
7e45049e94 refactor(VocabLessonView): combine lesson description and learning goals into a single component
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Merged the lesson description and learning goals sections into a unified block for improved layout and readability.
- Updated styles to enhance the visual presentation of the combined content, ensuring a more cohesive user experience.
2026-04-01 08:06:06 +02:00
Torsten Schulz (local)
1e801b33a5 refactor(AppSectionBar): update localization keys for section labels and titles
All checks were successful
Deploy to production / deploy (push) Successful in 2m58s
- Changed localization keys in AppSectionBar.vue to simplify and standardize the structure by removing the 'general' prefix.
- Updated section labels and title mappings to reflect the new key structure, enhancing consistency across the application.
2026-03-31 18:10:18 +02:00
Torsten Schulz (local)
c6caeefb5f feat(bisaya-course): enhance German course content and localization support
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s
- Updated the create-german-for-bisaya-course-content.js script to improve lesson pattern retrieval by introducing a new function for generating a lesson pattern pool.
- Added new exercises for various topics including 'Wohnung & Nachbarn', 'Besuch empfangen', 'Arzt, Apotheke, Termin', and 'Amt, Dokumente, Anmeldung', enhancing practical language skills for learners.
- Improved localization by integrating translation keys for various UI elements and error messages across multiple components, ensuring a consistent user experience in both German and Bisaya.
- Enhanced the main.js file to recognize Bisaya language preferences in browser settings, improving accessibility for users.
2026-03-31 17:40:03 +02:00
Torsten Schulz (local)
b1990334b9 feat(falukant): enhance relationship state handling and director updates
All checks were successful
Deploy to production / deploy (push) Successful in 2m58s
- Updated FalukantService to return user information alongside relationship state for improved context in relationship management.
- Modified DirectorInfo component to trigger a refresh when a new director is hired, ensuring up-to-date information.
- Added socket event handling for 'directorchanged' in BranchView to manage director updates effectively, enhancing real-time responsiveness in the application.
2026-03-31 12:09:55 +02:00
Torsten Schulz (local)
3187a6e7b0 feat(vocab): add lesson and completed lesson vocab pool endpoints
All checks were successful
Deploy to production / deploy (push) Successful in 3m5s
- Implemented new endpoints in VocabController for retrieving vocab pools based on lessons and completed lessons.
- Updated vocabRouter to include routes for accessing lesson vocab pools and completed lesson vocab pools.
- Enhanced VocabService with methods to extract vocab from exercises and lesson didactics, improving vocabulary retrieval for users.
- Modified VocabPracticeDialog and VocabCourseView components to support new vocab pool functionalities, enhancing user experience in vocabulary practice.
2026-03-31 11:59:48 +02:00
Torsten Schulz (local)
01293b0102 feat(bisaya-course): update German course creation script to support default owner retrieval
All checks were successful
Deploy to production / deploy (push) Successful in 3m12s
- Modified the create-german-for-bisaya-course.js script to allow optional ownerHashedId parameter.
- Implemented a new function to find a default owner ('system' or 'admin') if no ownerHashedId is provided.
- Updated documentation to reflect the change in usage for the script.
2026-03-31 11:42:23 +02:00
Torsten Schulz (local)
0d625f1727 feat(falukant): add age information to lovers in family view
All checks were successful
Deploy to production / deploy (push) Successful in 3m7s
- Updated FalukantService to include age details for partners in relationships.
- Added translations for 'age' in English, German, and Spanish localization files.
- Enhanced FamilyView component to display age information for lovers and candidates, improving user experience.
2026-03-31 11:36:12 +02:00
Torsten Schulz (local)
db0e80a559 feat(falukant): enhance child details with other parent information and birth context
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
- Updated FalukantService to include father and mother character IDs in child relationships.
- Added logic to retrieve and display other parent details in ChildDetailsDialog and FamilyView components.
- Introduced new translations for 'other parent' and 'birth context' in English, German, and Spanish localization files.
- Enhanced UI to show other parent information and birth context in child detail views.
2026-03-31 10:29:22 +02:00
Torsten Schulz (local)
9b3898e43c refactor(deploy): update deployment workflow for production
All checks were successful
Deploy to production / deploy (push) Successful in 3m1s
- Renamed workflow from "Deploy yourpart (blue-green)" to "Deploy to production" for clarity.
- Removed environment variables for SSH configuration and replaced them with direct references to secrets for improved security.
- Updated SSH connection and deployment script steps to utilize secrets for host, port, and user, enhancing the deployment process.
2026-03-31 10:14:09 +02:00
Torsten Schulz (local)
dba9eb8692 feat(bisaya-course): enhance evening and sleep routines in course content
Some checks failed
Deploy yourpart (blue-green) / deploy (push) Failing after 0s
- Updated the Bisaya course to include new evening greetings and sleep-related phrases in core patterns.
- Added multiple exercises focusing on recognizing and using these phrases, improving practical language skills for learners.
- Expanded lesson didactics to incorporate prompts for evening and sleep routines, enriching the overall learning experience.
2026-03-31 10:13:12 +02:00
Torsten Schulz (local)
734b8f9463 feat(bisaya-course): enhance evening greetings and sleep-related exercises
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
- Updated core patterns to include evening greetings and sleep phrases in the Bisaya course content.
- Added new exercises focusing on recognizing and using evening greetings and sleep wishes, enhancing practical language use for learners.
- Expanded lesson didactics to incorporate evening and sleep-related conversational prompts, improving the overall learning experience.
2026-03-31 09:39:46 +02:00
Torsten Schulz (local)
6b3aee458a feat(bisaya-course): expand exercises for shopping, neighborhood visits, conflict resolution, and free speaking
All checks were successful
Deploy to production / deploy (push) Successful in 2m57s
- Added new exercises in multiple-choice, gap-fill, and situational response formats for categories including 'Einkaufen vertiefen', 'Nachbarschaft & Besuche', 'Rollenspiel - Konflikt und Hilfe', and 'Freies Sprechen - Alltag ohne Stütze'.
- Each exercise includes detailed instructions, question data, answer data, and explanations to enhance the learning experience for Bisaya language learners.
- Focused on practical scenarios to improve conversational skills and vocabulary retention.
2026-03-31 09:04:24 +02:00
Torsten Schulz (local)
53f0745faf feat(bisaya-course): add new exercises for various everyday scenarios
All checks were successful
Deploy to production / deploy (push) Successful in 2m44s
- Introduced multiple-choice, gap-fill, and situational response exercises across several categories including 'Besuch & Gastfreundschaft', 'Gesundheit im Alltag', 'Unterwegs & Transport', 'Kinder im Alltag', and 'Arzt & Termin'.
- Each category includes detailed instructions, question data, answer data, and explanations to enhance learning experiences.
- Expanded the vocabulary and situational context for learners to practice real-life interactions in Bisaya.
2026-03-31 08:56:36 +02:00
Torsten Schulz (local)
9a78bc7c4b feat(admin): add potential fathers retrieval for character management
All checks were successful
Deploy to production / deploy (push) Successful in 2m47s
- Implemented a new method in AdminService to fetch potential fathers for a given character based on existing relationships.
- Updated AdminController to expose this functionality via a new API endpoint.
- Enhanced adminRouter to include the route for retrieving potential fathers.
- Modified frontend components to allow selection of potential fathers during pregnancy and birth management.
- Updated internationalization files to include new translation keys related to father selection.
2026-03-31 08:50:56 +02:00
Torsten Schulz (local)
ee11a989a0 refactor(update-backend): remove temporary environment directory cleanup
All checks were successful
Deploy to production / deploy (push) Successful in 2m43s
- Eliminated the removal of the temporary environment directory in the update-backend.sh script to retain the environment for potential debugging or further use.
2026-03-31 07:50:19 +02:00
Torsten Schulz (local)
9aad42655e refactor(update-backend): clean up script by removing unnecessary newline
Some checks failed
Deploy to production / deploy (push) Failing after 1m56s
- Removed an extra newline in the update-backend.sh script to improve readability and maintainability.
2026-03-31 07:35:47 +02:00
Torsten Schulz (local)
ca33a29317 refactor(update-backend): streamline .env file handling process
Some checks failed
Deploy to production / deploy (push) Failing after 1m58s
- Replaced the previous method of copying .env files with a direct installation from a specified source path.
- Improved permissions and ownership settings for the .env file to enhance security.
- Added user feedback for successful file transfer and verification steps.
2026-03-30 16:19:00 +02:00
Torsten Schulz (local)
4ebff4dc17 fix(i18n): correct translation directions in socialnetwork.json files
All checks were successful
Deploy to production / deploy (push) Successful in 3m24s
- Updated translation keys in German, English, and Spanish localization files to accurately reflect the intended language direction for vocabulary translation.
- Adjusted the `translateTo` and `translateFrom` keys to ensure consistency across all language files.
2026-03-30 16:03:23 +02:00
Torsten Schulz (local)
3d9bca099c feat(vocab): add vocab distractor pool functionality
All checks were successful
Deploy to production / deploy (push) Successful in 3m9s
- Implemented a new endpoint to retrieve a pool of distractors for vocabulary exercises based on prior lessons.
- Updated the VocabController and VocabRouter to include the new getVocabDistractorPool method.
- Enhanced VocabService to classify questions and gather distractors from previous lessons.
- Modified VocabLessonView to fetch and utilize the distractor pool for multiple-choice exercises, improving the learning experience.
2026-03-30 15:13:10 +02:00
Torsten Schulz (local)
2b83c45e97 feat(family): enhance family view and character pregnancy handling
All checks were successful
Deploy to production / deploy (push) Successful in 2m48s
- Updated the FalukantCharacter model to include a default scope that excludes pregnancy-related fields for compatibility with older databases.
- Implemented a new method in FalukantService to conditionally retrieve pregnancy information based on database schema.
- Enhanced the FamilyView component to display a summary navigation for family relationships, including partners, children, and lovers.
- Updated internationalization files to include new translations for family-related terms and summaries.
2026-03-30 14:36:02 +02:00
Torsten Schulz (local)
f35db4b1a1 feat(update-backend): add .env file handling from live system
All checks were successful
Deploy to production / deploy (push) Successful in 5m35s
- Implemented functionality to copy the .env file from the live system to the backend directory if it exists.
- Added ownership change for the copied .env file to ensure proper permissions.
- Included user feedback for successful or failed .env file transfer.
2026-03-30 14:16:15 +02:00
Torsten Schulz (local)
c52d4b60f9 feat(admin): implement pregnancy and birth management features
Some checks failed
Deploy to production / deploy (push) Failing after 2m6s
- Added new admin functionalities to force pregnancy, clear pregnancy, and trigger birth for characters.
- Introduced corresponding routes and controller methods in adminRouter and adminController.
- Enhanced the FalukantCharacter model to include pregnancy-related fields.
- Created database migration for adding pregnancy columns to the character table.
- Updated frontend views and internationalization files to support new pregnancy and birth management features.
- Improved user feedback and error handling for these new actions.
2026-03-30 13:44:43 +02:00
Torsten Schulz (local)
b2591da428 refactor(update scripts): enhance directory handling and error management
Some checks failed
Deploy to production / deploy (push) Failing after 1m46s
- Updated update-backend.sh and update-frontend.sh to accept a target directory as a parameter, improving flexibility.
- Refactored paths to use the target directory for all operations, ensuring consistency and reducing hardcoded values.
- Added error handling with 'set -euo pipefail' for better script reliability.
- Improved user feedback by displaying the target directory during updates.
2026-03-30 13:19:11 +02:00
Torsten Schulz (local)
5001292616 fix(deploy.yml): update deployment script path for blue-green deployment
Some checks failed
Deploy to production / deploy (push) Failing after 1m55s
- Changed the deployment script path from 'actualize-yourpart.sh' to 'deploy-yourpart-bluegreen.sh' to reflect the new deployment strategy.
2026-03-30 11:48:49 +02:00
Torsten Schulz (local)
8f461d4dba fix(deploy.yml): enhance SSH connection testing for deployment
All checks were successful
Deploy to production / deploy (push) Successful in 1m8s
- Added options to disable strict host key checking and enable batch mode for improved SSH connection reliability during deployment.
2026-03-30 11:40:00 +02:00
Torsten Schulz (local)
ed4c4e1b40 refactor(deploy.yml): improve SSH deployment process
Some checks failed
Deploy to production / deploy (push) Has been cancelled
- Added steps to prepare SSH keys and known hosts for secure connections.
- Implemented a test for the SSH connection to ensure successful deployment.
- Updated the deployment step to use SSH for executing the deployment script on the production server.
2026-03-30 11:35:34 +02:00
Torsten Schulz (local)
4ed9ea39a0 refactor(update.sh): enhance script functionality and user feedback
Some checks failed
Deploy to production / deploy (push) Failing after 1s
- Improved error handling to ensure the script exits on failure.
- Added a check for the current branch to ensure it is 'main' before proceeding.
- Updated git commands for better state management and clarity in user feedback messages.
2026-03-30 11:32:51 +02:00
Torsten Schulz (local)
0749c733a4 refactor(update.sh): improve update script reliability and clarity
Some checks failed
Deploy to production / deploy (push) Failing after 1s
- Changed shebang to use env for better portability.
- Added error handling to ensure the script exits on failure.
- Included a check to confirm the current branch is 'main' before proceeding.
- Updated git commands to fetch and reset to origin/main, ensuring a clean state.
- Enhanced user feedback messages for clarity.
2026-03-30 10:51:57 +02:00
Torsten Schulz (local)
966c73dda9 style(AppSectionBar): enhance layout and styling for improved visual presentation
- Updated .app-section-bar__copy to use flexbox for better alignment and spacing.
- Added gap and flex-wrap properties to improve layout responsiveness.
- Adjusted styles in .app-section-bar__eyebrow for a more cohesive design.
2026-03-30 10:45:49 +02:00
Torsten Schulz (local)
34f22229bb refactor(StatusBar): improve layout and styling for better user experience
- Removed unnecessary label from character name display for a cleaner look.
- Adjusted grid layout and spacing in the StatusBar for enhanced alignment and responsiveness.
- Refined padding and gap settings to optimize visual presentation of status elements.
2026-03-30 10:40:57 +02:00
Torsten Schulz (local)
05b91284fa refactor(StatusBar, PoliticsView): update UI elements and improve layout
- Modified StatusBar component to enhance character name display with a label suffix for clarity.
- Refactored styles in StatusBar for better alignment and spacing, improving overall visual presentation.
- Removed the PoliticsView hero section to streamline the layout and focus on tab content, enhancing user navigation.
2026-03-30 10:36:55 +02:00
Torsten Schulz (local)
c531ae2caf feat(StatusBar, falukantService): enhance character information display and UI structure
- Updated falukantService to include predefined first and last names in character data retrieval.
- Refactored StatusBar component layout to improve organization and visual clarity, introducing sections for identity and stats.
- Enhanced character name handling to prioritize display names, ensuring a more dynamic user experience.
2026-03-30 10:31:59 +02:00
Torsten Schulz (local)
028da5a9f0 feat(MessagesDialog, StatusBar, i18n): enhance character death notifications and UI updates
- Added support for displaying character death notifications with detailed information including age, region, and relationships in both German and English.
- Updated MessagesDialog to format additional parameters such as region, spouses, children, and lovers based on the locale.
- Enhanced StatusBar to show the character's name dynamically, improving user experience.
- Modified i18n files to include new notification messages for character deaths, ensuring accurate translations for both languages.
2026-03-30 10:25:54 +02:00
Torsten Schulz (local)
10d8ee015c feat(VocabLessonView): improve vocab question generation and answer validation
- Introduced a new method to retrieve equivalent vocabulary answers based on user prompts and direction, enhancing the accuracy of answer options.
- Updated the buildChoiceOptions method to handle multiple correct answers and ensure the prompt is excluded from choices.
- Enhanced the current vocab question structure to support multiple acceptable answers, improving user feedback and engagement during exercises.
2026-03-30 08:45:59 +02:00
Torsten Schulz (local)
c9a7619737 feat(VocabLessonView): refine vocab trainer experience and improve user guidance
- Updated vocab trainer descriptions and button labels to better reflect user progress and encourage engagement.
- Enhanced visual indicators for exercise availability and review priorities, clarifying the learning path for users.
- Introduced new methods to calculate trainer targets and review blending, optimizing the learning experience based on user performance.
2026-03-28 23:41:25 +01:00
Torsten Schulz (local)
a2c86247b6 feat(VocabLessonView): enhance exercise access and vocab trainer experience
- Added conditional access to exercises based on user progress and previous vocab completion.
- Introduced visual indicators for exercise availability and review priorities to guide users.
- Updated vocab trainer descriptions and button labels to reflect the user's current state and encourage engagement.
- Implemented methods to manage exercise unlock conditions and improve user feedback on progress.
2026-03-28 13:11:05 +01:00
Torsten Schulz (local)
8a96951b50 feat(vocabService): enhance answer validation for multiple choice exercises
- Improved answer validation logic by expanding multiple choice answer data based on lesson context.
- Added methods to extract indices of correct answers and normalize question prompts for better accuracy.
- Refactored existing answer checking to accommodate new data handling, ensuring robust evaluation of user responses.
2026-03-28 12:46:34 +01:00
Torsten Schulz (local)
71e120bf20 feat(socialnetwork): enhance folder and video management with user visibility options
- Added functionality to manage selected users for adult folders and erotic videos, allowing for more granular visibility control.
- Introduced new endpoints and methods in the SocialNetworkController and SocialNetworkService to handle selected users.
- Updated the frontend components to include input fields for selected users in CreateFolderDialog, EditImageDialog, and EroticPicturesView.
- Enhanced the routing to support fetching erotic folders and videos by username, improving user experience in profile views.
2026-03-27 16:56:45 +01:00
Torsten Schulz (local)
39032570e3 fix(i18n): correct German translations for video upload hints and descriptions
- Updated the German translations in socialnetwork.json for improved accuracy and clarity.
- Enhanced the text in EroticPicturesView.vue and EroticVideosView.vue to ensure proper spelling and grammar in user-facing messages.
2026-03-27 16:29:18 +01:00
Torsten Schulz (local)
9e8f8e8077 refactor(i18n): update video management texts for clarity and consistency
- Revised the introductory texts and hints for video uploads in German, English, and Spanish to enhance user understanding.
- Improved descriptions to better reflect the functionality of the video management features.
- Removed outdated statistics display from the EroticVideosView component to streamline the user interface.
2026-03-27 15:43:15 +01:00
Torsten Schulz (local)
e76be33743 refactor(EroticVideosView): restructure layout and enhance video statistics display
- Updated the layout of the EroticVideosView component to improve organization and user experience.
- Introduced a sidebar for video upload and management, separating it from the video list.
- Added statistics for total, visible, and hidden videos to provide users with better insights.
- Enhanced form elements and labels for clarity and usability during video uploads.
2026-03-27 15:36:00 +01:00
Torsten Schulz (local)
6cbcf9d95f style(DialogWidget): update dialog overlay class for improved modal handling
- Changed the class binding for the dialog overlay to differentiate between modal and non-modal states, enhancing visual clarity.
- Introduced a new class for non-modal dialogs to ensure proper positioning and z-index management.
2026-03-27 15:21:57 +01:00
Torsten Schulz (local)
31a96aaf60 style(DialogWidget): remove backdrop filter for non-modal overlay
- Updated the non-modal dialog overlay to have no backdrop filter, enhancing visual clarity.
- Set the modal property to false in MessagesDialog to ensure proper dialog behavior.
2026-03-27 15:17:18 +01:00
Torsten Schulz (local)
8a1ff52a61 feat(chat): enhance room entry announcement logic in MultiChatDialog.vue
- Introduced a new property `lastAnnouncedRoomName` to track the last announced room, preventing redundant announcements.
- Updated the room entry announcement logic to ensure messages are sent only when entering a new room, improving clarity for users.
2026-03-27 15:10:38 +01:00
Torsten Schulz (local)
84c598bf52 feat(chat): initialize current room name in MultiChatDialog.vue
- Added a new property `currentRoomName` to track the name of the currently selected room.
- Updated the initialization logic to reset `currentRoomName` when rooms are loaded and when the chat is disconnected, ensuring accurate room state management.
2026-03-27 15:03:32 +01:00
Torsten Schulz (local)
291e79c41f fix(chat): improve room announcement logic in MultiChatDialog.vue
- Enhanced the logic for announcing room entries, particularly in adult-only mode, to ensure accurate messaging based on room selection.
- Added checks to prevent redundant announcements and ensure proper synchronization with the selected room.
2026-03-27 14:46:08 +01:00
Torsten Schulz (local)
3b823420e6 feat(chat): implement room synchronization for adult-only mode in MultiChatDialog.vue
- Added logic to handle room synchronization when entering a chat in adult-only mode.
- Introduced a flag to manage ongoing room sync requests, improving user experience during room transitions.
2026-03-27 14:37:49 +01:00
Torsten Schulz (local)
674c4d0b69 refactor(chat): simplify adult-only mode checks in MultiChatDialog.vue
- Removed redundant checks for adult-only mode when opening the chat dialog and connecting the chat socket.
- Streamlined the logic to enhance readability and maintainability of the component.
2026-03-27 14:29:00 +01:00
Torsten Schulz (local)
9f3facbb3f chore(dependencies): update package-lock.json and package.json for dependency versions
- Upgraded @emnapi/runtime to version 1.9.1 and @img/colour to version 1.1.0 in package-lock.json.
- Added new dependencies for ansi-regex and strip-ansi with updated versions in multiple locations.
- Updated lodash types to version 4.17.24 and ansi-escapes to version 7.3.0.
- Introduced overrides for minimatch and tmp in package.json to ensure consistent behavior across environments.
- Refactored MultiChatDialog.vue to improve room selection logic and handle adult-only mode more effectively.
2026-03-27 14:05:29 +01:00
Torsten Schulz (local)
07604cc9fa feat(navigation): enhance adult verification handling and notifications
- Updated navigationController to simplify the eroticChat menu structure.
- Enhanced adminService to notify users of adult verification status changes, including previous status.
- Improved AppNavigation and related components to register and unregister socket listeners for adult verification updates.
- Added localized messages for adult verification notifications in English, German, and Spanish.
- Introduced a verification hint in the EroticAccessView to guide users on document submission.
2026-03-27 13:23:44 +01:00
Torsten Schulz (local)
82223676a6 style(admin): update button styles in AdultVerificationView and EroticModerationView
- Added new styles for buttons in the adult verification filters to enhance appearance and usability.
- Set minimum height, padding, and font size for consistency across both views.
2026-03-27 11:25:56 +01:00
Torsten Schulz (local)
207ef6266a feat(deploy): add adult verification directory creation and permissions setup
- Enhanced deploy-backend.sh and update-backend.sh to create the adult verification directory under /opt/yourpart-data.
- Updated permissions for the new directory to ensure proper access control.
- Refactored file path handling in AdminService and SettingsService to utilize the new directory structure for adult verification files.
2026-03-27 11:24:21 +01:00
Torsten Schulz (local)
02837c7b73 refactor(admin): restructure adult verification and erotic moderation views for improved layout
- Updated the AdultVerificationView and EroticModerationView components to utilize a new layout structure with content scrolling and hidden overflow for better user experience.
- Adjusted styles in styles.scss to support the new layout, ensuring proper height and overflow handling for content sections.
2026-03-27 11:18:42 +01:00
Torsten Schulz (local)
25b658acce feat(admin): enhance adult verification file handling and localization
- Added a new method in AdminService to resolve adult verification file paths, improving file retrieval logic.
- Updated the AdminVerificationView to display a message when the verification document is missing.
- Localized the missing document message in German, English, and Spanish for better user experience.
2026-03-27 11:08:52 +01:00
Torsten Schulz (local)
0f0c102ded fix(admin): add 'adult_verification_request' to user parameter type filtering
- Updated the filtering criteria in the AdminService to include 'adult_verification_request' in the user parameter type query, enhancing data retrieval for adult verification processes.
2026-03-27 10:57:16 +01:00
Torsten Schulz (local)
26eb7b8ce7 feat(admin): add document preview functionality to adult verification
- Implemented a preview section for adult verification documents, allowing users to view images and PDFs inline.
- Added localization support for preview titles and messages in German, English, and Spanish.
- Enhanced the component's state management to handle preview visibility and cleanup.
2026-03-27 10:50:28 +01:00
Torsten Schulz (local)
0dd2bce5d1 fix(settings): streamline settings type creation in settingsService and initialization
- Refactored settingsService to use findOrCreate for settings type, improving efficiency and error handling.
- Added initialization for 'account' settings type in initializeSettings, ensuring all necessary settings are created during setup.
2026-03-27 10:43:44 +01:00
Torsten Schulz (local)
cf6d72385e fix(settings): enhance user parameter handling and add special user parameter types
- Introduced a new method to ensure special user parameter types for adult verification settings, improving data integrity and handling.
- Updated the upsertUserParam method to utilize the new special parameter type handling, ensuring robust user parameter management.
- Updated package dependencies in package.json and package-lock.json for consistency and to address potential vulnerabilities.
2026-03-27 10:38:42 +01:00
Torsten Schulz (local)
1a86061680 fix(dependencies): update @gltf-transform packages and sequelize-cli version
- Upgraded @gltf-transform/cli, core, extensions, and functions to version 4.3.0 for improved functionality and compatibility.
- Updated sequelize-cli to version 6.6.5 to address potential vulnerabilities.
- Added an overrides section in package.json for minimatch to ensure consistent behavior across environments.
- Refactored the EroticAccessView.vue component for improved structure and readability.
2026-03-27 10:23:25 +01:00
Torsten Schulz (local)
e13deb0720 fix(api): improve 404 handling for unknown API routes
- Refactored the 404 response for API routes to ensure it only triggers for paths starting with '/api/', enhancing clarity in error handling.
- Updated package dependencies in package.json and package-lock.json to maintain version consistency and address potential vulnerabilities.
2026-03-27 09:52:24 +01:00
Torsten Schulz (local)
21072139f7 fix(cors): refine CORS handling for OPTIONS requests
- Updated CORS middleware to explicitly handle OPTIONS requests, ensuring proper preflight response and improving API request handling.
2026-03-27 09:49:54 +01:00
Torsten Schulz (local)
1878b2a8c7 fix(router): refine stock and inventory routes for improved clarity
- Updated stock and inventory routes to explicitly define branchId as a required parameter, enhancing API usability and consistency in request handling.
2026-03-27 09:39:51 +01:00
Torsten Schulz (local)
6563ca23c7 fix(router): update stock and inventory routes to support optional branchId parameter
- Modified the stock and inventory routes to allow an optional branchId parameter, improving flexibility in API requests.
2026-03-27 09:24:05 +01:00
392 changed files with 53061 additions and 3331 deletions

0
.codex Normal file
View File

View File

@@ -0,0 +1,9 @@
---
description: C++-Worker unter src/ sind obsolet — nicht erweitern oder als Quelle für Spiellogik nutzen
alwaysApply: true
---
# Legacy C++ (`src/`)
- Verzeichnis **`src/`** (C++-Worker, WebSocket-Server): **obsolet**. Keine neuen Features, keine fachlichen Fixes dort planen oder umsetzen, sofern der Nutzer nicht ausdrücklich etwas anderes verlangt.
- Falukant-Hintergrundlogik: **Backend** (`backend/`), **externer Daemon**, **Frontend** — siehe `docs/LEGACY_CPP_WORKERS.md`.

121
.gitea/workflows/deploy.yml Normal file
View File

@@ -0,0 +1,121 @@
name: Deploy to production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect vocab course changes
id: vocab_course_changes
shell: bash
run: |
set -euo pipefail
BASE="${{ gitea.event.before }}"
HEAD="${{ gitea.sha }}"
if [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
BASE="HEAD~1"
fi
git diff --name-only "$BASE" "$HEAD" > changed-files.txt
cat changed-files.txt
COMMIT_MESSAGE="$(git log -1 --pretty=%B "$HEAD" || true)"
if echo "$COMMIT_MESSAGE" | grep -qi '\[force-deploy\]'; then
echo "force_deploy=true" >> "$GITHUB_OUTPUT"
else
echo "force_deploy=false" >> "$GITHUB_OUTPUT"
fi
if grep -E '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))' changed-files.txt; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
if grep -E '^frontend/' changed-files.txt >/dev/null; then
echo "frontend_changed=true" >> "$GITHUB_OUTPUT"
else
echo "frontend_changed=false" >> "$GITHUB_OUTPUT"
fi
if grep -E '^backend/' changed-files.txt \
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab)$' >/dev/null; then
echo "backend_app_changed=true" >> "$GITHUB_OUTPUT"
else
echo "backend_app_changed=false" >> "$GITHUB_OUTPUT"
fi
# App-Code-Änderungen, die einen echten Deploy benötigen
# (Frontend oder Backend außerhalb reiner Kurs-/Dokument-Sync-Dateien)
if grep -E '^(frontend/|backend/)' changed-files.txt \
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))'; then
echo "app_changed=true" >> "$GITHUB_OUTPUT"
else
echo "app_changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Prepare SSH
run: |
mkdir -p ~/.ssh
printf "%s" "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "${{ secrets.PROD_PORT }}" "${{ secrets.PROD_HOST }}" >> ~/.ssh/known_hosts
- name: Test SSH connection
run: |
ssh -i ~/.ssh/id_ed25519 \
-o StrictHostKeyChecking=no \
-o BatchMode=yes \
-p "${{ secrets.PROD_PORT }}" \
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
"echo SSH OK"
- name: Run deployment script
if: steps.vocab_course_changes.outputs.app_changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
run: |
DEPLOY_FLAGS=""
if [ "${{ steps.vocab_course_changes.outputs.force_deploy }}" = "true" ]; then
DEPLOY_FLAGS=""
elif [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" != "true" ]; then
DEPLOY_FLAGS="--skip-frontend"
elif [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" != "true" ]; then
DEPLOY_FLAGS="--skip-backend"
fi
DEPLOY_TARGET="${{ secrets.PROD_DEPLOY_TARGET }}"
if [ -z "$DEPLOY_TARGET" ]; then
DEPLOY_TARGET="/opt/yourpart-green"
fi
echo "Deploy-Flags: ${DEPLOY_FLAGS:-<none>}"
echo "Deploy-Target: $DEPLOY_TARGET"
ssh -i ~/.ssh/id_ed25519 \
-p "${{ secrets.PROD_PORT }}" \
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
"/home/tsschulz/deploy-yourpart-bluegreen.sh ${DEPLOY_TARGET} ${DEPLOY_FLAGS}"
- name: Skip full deployment (no app changes)
if: steps.vocab_course_changes.outputs.app_changed != 'true' && steps.vocab_course_changes.outputs.force_deploy != 'true'
run: |
echo "Kein Full-Deploy: Es wurden keine Frontend/Backend-App-Dateien geändert."
- name: Sync vocab course content
if: steps.vocab_course_changes.outputs.changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
run: |
ssh -i ~/.ssh/id_ed25519 \
-p "${{ secrets.PROD_PORT }}" \
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
"cd /opt/yourpart && npm --prefix backend run sync:vocab-courses"

3
.gitignore vendored
View File

@@ -17,6 +17,9 @@ frontend/node_modules
frontend/node_modules/* frontend/node_modules/*
frontend/dist frontend/dist
frontend/dist/* frontend/dist/*
frontend/scripts/.i18n-de-fr-cache.json
frontend/scripts/.falukant-fr-smooth-cache.json
frontend/ceb-locale-audit-report.json
frontedtree.txt frontedtree.txt
backend/dist/ backend/dist/
backend/data/model-cache backend/data/model-cache

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
## zum testen des push
Hinweis: Das Verzeichnis **`src/`** (C++-Worker) ist veraltet; siehe [`docs/LEGACY_CPP_WORKERS.md`](docs/LEGACY_CPP_WORKERS.md).

View File

@@ -3,7 +3,7 @@
This project now supports a per-region sales tax (`tax_percent`) for Falukant. This project now supports a per-region sales tax (`tax_percent`) for Falukant.
Migration Migration
- A SQL migration was added: `backend/migrations/20260101000000-add-tax-percent-to-region.cjs`. - A SQL migration was added: `backend/migrations-archive/20260101000000-add-tax-percent-to-region.cjs`.
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`. - It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
Runtime configuration Runtime configuration

View File

@@ -23,6 +23,7 @@ import vocabRouter from './routers/vocabRouter.js';
import dashboardRouter from './routers/dashboardRouter.js'; import dashboardRouter from './routers/dashboardRouter.js';
import newsRouter from './routers/newsRouter.js'; import newsRouter from './routers/newsRouter.js';
import calendarRouter from './routers/calendarRouter.js'; import calendarRouter from './routers/calendarRouter.js';
import moderationRouter from './routers/moderationRouter.js';
import cors from 'cors'; import cors from 'cors';
import './jobs/sessionCleanup.js'; import './jobs/sessionCleanup.js';
@@ -83,7 +84,12 @@ const corsOptions = {
}; };
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.options('*', cors(corsOptions)); app.use((req, res, next) => {
if (req.method === 'OPTIONS') {
return cors(corsOptions)(req, res, next);
}
return next();
});
app.use(express.json()); // To handle JSON request bodies app.use(express.json()); // To handle JSON request bodies
app.use('/api/chat', chatRouter); app.use('/api/chat', chatRouter);
@@ -100,6 +106,7 @@ app.use('/api/contact', contactRouter);
app.use('/api/socialnetwork', socialnetworkRouter); app.use('/api/socialnetwork', socialnetworkRouter);
app.use('/api/vocab', vocabRouter); app.use('/api/vocab', vocabRouter);
app.use('/api/forum', forumRouter); app.use('/api/forum', forumRouter);
app.use('/api/moderation', moderationRouter);
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/models', modelsProxyRouter);
@@ -124,6 +131,11 @@ app.get(/^\/(?!api\/).*/, (req, res) => {
}); });
// Fallback 404 for unknown API routes // Fallback 404 for unknown API routes
app.use('/api/*', (req, res) => res.status(404).send('404 Not Found')); app.use((req, res, next) => {
if (req.path.startsWith('/api/')) {
return res.status(404).send('404 Not Found');
}
return next();
});
export default app; export default app;

View File

@@ -0,0 +1,33 @@
const path = require('path');
const dotenv = require('dotenv');
const envPath = process.env.SEQUELIZE_ENV_FILE
? path.resolve(process.cwd(), process.env.SEQUELIZE_ENV_FILE)
: path.resolve(process.cwd(), '.env');
dotenv.config({ path: envPath });
const dialectOptions = {};
if (process.env.DB_SSL === '1' || process.env.PGSSLMODE === 'require') {
dialectOptions.ssl = process.env.DB_SSL_REJECT_UNAUTHORIZED === '0'
? { rejectUnauthorized: false }
: true;
}
// pg/SCRAM: password muss ein String sein; bei fehlender .env sonst undefined-Fallen vermeiden
const shared = {
username: process.env.DB_USER != null ? String(process.env.DB_USER) : undefined,
password: process.env.DB_PASS != null ? String(process.env.DB_PASS) : '',
database: process.env.DB_NAME != null ? String(process.env.DB_NAME) : undefined,
host: process.env.DB_HOST || '127.0.0.1',
port: Number.parseInt(process.env.DB_PORT || '5432', 10),
dialect: 'postgres',
logging: false,
dialectOptions
};
module.exports = {
development: shared,
test: shared,
production: shared
};

View File

@@ -13,6 +13,11 @@ class AdminController {
this.searchUser = this.searchUser.bind(this); this.searchUser = this.searchUser.bind(this);
this.getFalukantUserById = this.getFalukantUserById.bind(this); this.getFalukantUserById = this.getFalukantUserById.bind(this);
this.changeFalukantUser = this.changeFalukantUser.bind(this); this.changeFalukantUser = this.changeFalukantUser.bind(this);
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this);
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this); this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
this.updateFalukantStock = this.updateFalukantStock.bind(this); this.updateFalukantStock = this.updateFalukantStock.bind(this);
this.addFalukantStock = this.addFalukantStock.bind(this); this.addFalukantStock = this.addFalukantStock.bind(this);
@@ -29,6 +34,10 @@ class AdminController {
this.getUser = this.getUser.bind(this); this.getUser = this.getUser.bind(this);
this.getUsers = this.getUsers.bind(this); this.getUsers = this.getUsers.bind(this);
this.updateUser = this.updateUser.bind(this); this.updateUser = this.updateUser.bind(this);
this.resetUserVocabLessonProgress = this.resetUserVocabLessonProgress.bind(this);
this.markUserVocabLessonsCompleteThrough = this.markUserVocabLessonsCompleteThrough.bind(this);
this.getUserVocabCourses = this.getUserVocabCourses.bind(this);
this.getVocabCourseForAdmin = this.getVocabCourseForAdmin.bind(this);
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this); this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
this.setAdultVerificationStatus = this.setAdultVerificationStatus.bind(this); this.setAdultVerificationStatus = this.setAdultVerificationStatus.bind(this);
this.getAdultVerificationDocument = this.getAdultVerificationDocument.bind(this); this.getAdultVerificationDocument = this.getAdultVerificationDocument.bind(this);
@@ -45,6 +54,9 @@ class AdminController {
// Statistics // Statistics
this.getUserStatistics = this.getUserStatistics.bind(this); this.getUserStatistics = this.getUserStatistics.bind(this);
this.getFalukantRegions = this.getFalukantRegions.bind(this); this.getFalukantRegions = this.getFalukantRegions.bind(this);
this.getFalukantAllRegions = this.getFalukantAllRegions.bind(this);
this.getFalukantRegionTypes = this.getFalukantRegionTypes.bind(this);
this.createFalukantRegion = this.createFalukantRegion.bind(this);
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this); this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
this.getRegionDistances = this.getRegionDistances.bind(this); this.getRegionDistances = this.getRegionDistances.bind(this);
this.upsertRegionDistance = this.upsertRegionDistance.bind(this); this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
@@ -125,6 +137,77 @@ class AdminController {
} }
} }
async resetUserVocabLessonProgress(req, res) {
const schema = Joi.object({
lessonId: Joi.number().integer().positive().required()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.adminResetUserVocabLessonProgress(requester, id, value.lessonId);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (err.message === 'lessonnotfound' ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
async markUserVocabLessonsCompleteThrough(req, res) {
const schema = Joi.object({
courseId: Joi.number().integer().positive().required(),
throughLessonNumber: Joi.number().integer().positive().required()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.adminMarkUserVocabLessonsCompleteThrough(
requester,
id,
value.courseId,
value.throughLessonNumber
);
res.status(200).json(result);
} catch (err) {
let status = 500;
if (err.message === 'noaccess') status = 403;
else if (err.message === 'notenrolled') status = 403;
else if (err.message === 'badrequest') status = 400;
res.status(status).json({ error: err.message });
}
}
async getUserVocabCourses(req, res) {
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.adminListUserEnrolledVocabCourses(requester, id);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (err.message === 'notfound' ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
async getVocabCourseForAdmin(req, res) {
try {
const { userid: requester } = req.headers;
const { courseId } = req.params;
const result = await AdminService.adminGetVocabCourseWithLessons(requester, courseId);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (err.message === 'coursenotfound' ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
async getAdultVerificationRequests(req, res) { async getAdultVerificationRequests(req, res) {
try { try {
const { userid: requester } = req.headers; const { userid: requester } = req.headers;
@@ -372,6 +455,77 @@ class AdminController {
} }
} }
async adminForceFalukantPregnancy(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId, fatherCharacterId, dueInDays } = req.body;
const response = await AdminService.adminForceFalukantPregnancy(userId, characterId, {
fatherCharacterId,
dueInDays,
});
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminGetPotentialFathersForCharacter(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.params;
const response = await AdminService.adminGetPotentialFathersForCharacter(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminClearFalukantPregnancy(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.body;
const response = await AdminService.adminClearFalukantPregnancy(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminForceFalukantBirth(req, res) {
try {
const { userid: userId } = req.headers;
const { motherCharacterId, fatherCharacterId, birthContext, legitimacy, gender } = req.body;
const response = await AdminService.adminForceFalukantBirth(userId, motherCharacterId, {
fatherCharacterId,
birthContext,
legitimacy,
gender,
});
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminCleanupCharacterDeathArtifacts(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.params;
const response = await AdminService.adminCleanupCharacterDeathArtifacts(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess'
? 403
: (['invalidCharacter', 'notfound', 'targetnotdead'].includes(error.message) ? 400 : 500);
res.status(status).json({ error: error.message });
}
}
async getFalukantUserBranches(req, res) { async getFalukantUserBranches(req, res) {
try { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
@@ -432,6 +586,42 @@ class AdminController {
} }
} }
async getFalukantAllRegions(req, res) {
try {
const { userid: userId } = req.headers;
const regions = await AdminService.getFalukantAllRegions(userId);
res.status(200).json(regions);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async getFalukantRegionTypes(req, res) {
try {
const { userid: userId } = req.headers;
const types = await AdminService.getFalukantRegionTypes(userId);
res.status(200).json(types);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async createFalukantRegion(req, res) {
try {
const { userid: userId } = req.headers;
const created = await AdminService.createFalukantRegion(userId, req.body || {});
res.status(200).json(created);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 400;
res.status(status).json({ error: error.message });
}
}
async updateFalukantRegionMap(req, res) { async updateFalukantRegionMap(req, res) {
try { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;

View File

@@ -29,6 +29,8 @@ class AuthController {
} catch (error) { } catch (error) {
if (error.message === 'credentialsinvalid') { if (error.message === 'credentialsinvalid') {
res.status(404).json({ error: error.message }); res.status(404).json({ error: error.message });
} else if (error.message === 'userblocked') {
res.status(403).json({ error: error.message });
} else { } else {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }

View File

@@ -16,6 +16,7 @@ class ChatController {
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this); this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
this.getOwnRooms = this.getOwnRooms.bind(this); this.getOwnRooms = this.getOwnRooms.bind(this);
this.deleteOwnRoom = this.deleteOwnRoom.bind(this); this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
this.reportChatIncident = this.reportChatIncident.bind(this);
} }
async getMessages(req, res) { async getMessages(req, res) {
@@ -215,6 +216,32 @@ class ChatController {
res.status(status).json({ error: error.message }); res.status(status).json({ error: error.message });
} }
} }
async reportChatIncident(req, res) {
const schema = Joi.object({
context: Joi.string().valid('random_chat', 'multi_chat', 'one_to_one').required(),
reporterHashedId: Joi.string().allow('', null),
reporterRandomId: Joi.string().allow('', null),
reporterUsername: Joi.string().allow('', null),
offenderHashedId: Joi.string().allow('', null),
offenderRandomId: Joi.string().allow('', null),
offenderUsername: Joi.string().allow('', null),
incidentAt: Joi.date().iso().required(),
chatHistory: Joi.array().min(1).required(),
metadata: Joi.object().unknown(true).optional()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const result = await chatService.reportChatIncident(value);
return res.status(201).json(result);
} catch (err) {
console.error('Error in reportChatIncident:', err);
return res.status(400).json({ error: err.message });
}
}
} }
export default ChatController; export default ChatController;

View File

@@ -1,4 +1,5 @@
import FalukantService from '../services/falukantService.js'; import FalukantService from '../services/falukantService.js';
import politicalPowersService from '../services/falukantPoliticalPowersService.js';
function extractHashedUserId(req) { function extractHashedUserId(req) {
return req.headers?.userid; return req.headers?.userid;
@@ -118,6 +119,8 @@ class FalukantController {
}); });
this.setLoverMaintenance = this._wrapWithUser((userId, req) => this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true }); this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
this.improveLoverAffection = this._wrapWithUser((userId, req) =>
this.service.improveLoverAffection(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
this.createLoverRelationship = this._wrapWithUser((userId, req) => this.createLoverRelationship = this._wrapWithUser((userId, req) =>
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true }); this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
this.spendTimeWithSpouse = this._wrapWithUser((userId) => this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
@@ -207,11 +210,29 @@ class FalukantController {
}, { blockInDebtorsPrison: true }); }, { blockInDebtorsPrison: true });
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId)); this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getPoliticalOfficeCatalog = this._wrapWithUser((userId) => this.service.getPoliticalOfficeCatalog(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId)); this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId)); this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true }); this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true }); this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true });
this.getPoliticalMyPowers = this._wrapWithUser((userId) => politicalPowersService.getMyPowers(userId));
this.getPoliticalTaxJurisdiction = this._wrapWithUser((userId) => politicalPowersService.getTaxJurisdiction(userId));
this.setPoliticalRegionTax = this._wrapWithUser((userId, req) =>
politicalPowersService.setRegionTax(userId, parseInt(req.params.regionId, 10), req.body?.percent), { blockInDebtorsPrison: true });
this.getPoliticalRegionTaxHistory = this._wrapWithUser((userId, req) =>
politicalPowersService.getRegionTaxHistory(userId, parseInt(req.params.regionId, 10), parseInt(req.query.limit || '5', 10)));
this.getPoliticalAppointableOffices = this._wrapWithUser((userId) => politicalPowersService.getAppointableOffices(userId));
this.createPoliticalAppointment = this._wrapWithUser(
(userId, req) =>
politicalPowersService.createAppointment(userId, {
targetCharacterId: req.body?.targetCharacterId,
officeTypeId: req.body?.officeTypeId,
regionId: req.body?.regionId
}),
{ successStatus: 201, blockInDebtorsPrison: true }
);
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId)); this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId)); this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
this.getProductPriceInRegion = this._wrapWithUser((userId, req) => { this.getProductPriceInRegion = this._wrapWithUser((userId, req) => {
@@ -227,7 +248,9 @@ class FalukantController {
if (Number.isNaN(regionId)) { if (Number.isNaN(regionId)) {
throw new Error('regionId is required'); throw new Error('regionId is required');
} }
return this.service.getAllProductPricesInRegion(userId, regionId); const networkWorth = req.query.networkWorth === '1' || req.query.networkWorth === 'true';
const branchId = req.query.branchId != null ? parseInt(req.query.branchId, 10) : null;
return this.service.getAllProductPricesInRegion(userId, regionId, { networkWorth, branchId });
}); });
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);
@@ -242,11 +265,17 @@ class FalukantController {
const body = req.body || {}; const body = req.body || {};
const items = Array.isArray(body.items) ? body.items : []; const items = Array.isArray(body.items) ? body.items : [];
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null; const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
const includeTransportCosts = body.includeTransportCosts === true || body.includeTransportCosts === 'true';
const valid = items.map(i => ({ const valid = items.map(i => ({
productId: parseInt(i.productId, 10), productId: parseInt(i.productId, 10),
currentPrice: parseFloat(i.currentPrice) currentPrice: parseFloat(i.currentPrice)
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice)); })).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId); return this.service.getProductPricesInCitiesBatch(
userId,
valid,
Number.isNaN(currentRegionId) ? null : currentRegionId,
{ includeTransportCosts }
);
}); });
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true }); this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true });
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true }); this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });

View File

@@ -0,0 +1,79 @@
import Joi from 'joi';
import moderationService from '../services/moderationService.js';
const moderationController = {
async createReport(req, res) {
const allowedTargetTypes = [
'forum_message',
'gallery_image',
'guestbook_entry',
'one_to_one_message',
'diary_entry',
'user_profile',
'blog',
'blog_post'
];
const schema = Joi.object({
targetType: Joi.string().valid(...allowedTargetTypes).required(),
targetId: Joi.number().integer().min(1).optional(),
targetRef: Joi.string().trim().max(255).allow('').optional(),
reason: Joi.string().trim().min(3).max(120).required(),
details: Joi.string().allow('').max(2000).optional()
}).or('targetId', 'targetRef');
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: userId } = req.headers;
const result = await moderationService.createReport(userId, value);
return res.status(201).json(result);
} catch (err) {
console.error('Error in createReport:', err);
return res.status(400).json({ error: err.message });
}
},
async listReports(req, res) {
try {
const { userid: userId } = req.headers;
const result = await moderationService.listReports(userId, req.query || {});
return res.status(200).json(result);
} catch (err) {
console.error('Error in listReports:', err);
return res.status(400).json({ error: err.message });
}
},
async updateReportStatus(req, res) {
const schema = Joi.object({
status: Joi.string().valid('open', 'in_review', 'resolved', 'rejected').required(),
reviewerNote: Joi.string().allow('').max(2000).optional()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: userId } = req.headers;
const result = await moderationService.updateReportStatus(userId, req.params.reportId, value);
return res.status(200).json(result);
} catch (err) {
console.error('Error in updateReportStatus:', err);
return res.status(400).json({ error: err.message });
}
},
async getOpenReportCount(req, res) {
try {
const { userid: userId } = req.headers;
const result = await moderationService.getOpenReportCount(userId);
return res.status(200).json(result);
} catch (err) {
console.error('Error in getOpenReportCount:', err);
return res.status(400).json({ error: err.message });
}
}
};
export default moderationController;

View File

@@ -96,9 +96,7 @@ const menuStructure = {
}, },
eroticChat: { eroticChat: {
visible: ["over18"], visible: ["over18"],
action: "openEroticChat", action: "openEroticChat"
view: "window",
class: "eroticChatWindow"
} }
} }
}, },
@@ -280,6 +278,10 @@ const menuStructure = {
visible: ["mainadmin", "forum"], visible: ["mainadmin", "forum"],
path: "/admin/forum" path: "/admin/forum"
}, },
moderationReports: {
visible: ["mainadmin", "forum"],
path: "/admin/moderation/reports"
},
chatrooms: { chatrooms: {
visible: ["mainadmin", "chatrooms"], visible: ["mainadmin", "chatrooms"],
path: "/admin/chatrooms" path: "/admin/chatrooms"
@@ -293,7 +295,7 @@ const menuStructure = {
path: "/admin/interests" path: "/admin/interests"
}, },
falukant: { falukant: {
visible: ["mainadmin", "falukant"], visible: ["mainadmin", "falukant", "worker_schedule_read"],
children: { children: {
logentries: { logentries: {
visible: ["mainadmin", "falukant"], visible: ["mainadmin", "falukant"],
@@ -315,6 +317,10 @@ const menuStructure = {
visible: ["mainadmin", "falukant"], visible: ["mainadmin", "falukant"],
path: "/admin/falukant/create-npc" path: "/admin/falukant/create-npc"
}, },
workerSchedules: {
visible: ["mainadmin", "worker_schedule_read"],
path: "/admin/falukant/worker-schedules"
},
} }
}, },
minigames: { minigames: {

View File

@@ -16,13 +16,16 @@ class SocialNetworkController {
this.getFoldersByUsername = this.getFoldersByUsername.bind(this); this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
this.deleteFolder = this.deleteFolder.bind(this); this.deleteFolder = this.deleteFolder.bind(this);
this.getAdultFolders = this.getAdultFolders.bind(this); this.getAdultFolders = this.getAdultFolders.bind(this);
this.getAdultFoldersByUsername = this.getAdultFoldersByUsername.bind(this);
this.createAdultFolder = this.createAdultFolder.bind(this); this.createAdultFolder = this.createAdultFolder.bind(this);
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this); this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
this.uploadAdultImage = this.uploadAdultImage.bind(this); this.uploadAdultImage = this.uploadAdultImage.bind(this);
this.getAdultImageByHash = this.getAdultImageByHash.bind(this); this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
this.changeAdultImage = this.changeAdultImage.bind(this); this.changeAdultImage = this.changeAdultImage.bind(this);
this.listEroticVideos = this.listEroticVideos.bind(this); this.listEroticVideos = this.listEroticVideos.bind(this);
this.getEroticVideosByUsername = this.getEroticVideosByUsername.bind(this);
this.uploadEroticVideo = this.uploadEroticVideo.bind(this); this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
this.changeEroticVideo = this.changeEroticVideo.bind(this);
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this); this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
this.reportEroticContent = this.reportEroticContent.bind(this); this.reportEroticContent = this.reportEroticContent.bind(this);
this.createGuestbookEntry = this.createGuestbookEntry.bind(this); this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
@@ -157,8 +160,8 @@ class SocialNetworkController {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;
const { imageId } = req.params; const { imageId } = req.params;
const { title, visibilities } = req.body; const { title, visibilities, selectedUsers } = req.body;
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities); const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities, selectedUsers);
console.log('--->', folderId); console.log('--->', folderId);
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId)); res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
} catch (error) { } catch (error) {
@@ -208,6 +211,21 @@ class SocialNetworkController {
} }
} }
async getAdultFoldersByUsername(req, res) {
try {
const requestingUserId = req.headers.userid;
const { username } = req.params;
const folders = await this.socialNetworkService.getAdultFoldersByUsername(username, requestingUserId);
if (!folders) {
return res.status(404).json({ error: 'No folders found or access denied.' });
}
res.status(200).json(folders);
} catch (error) {
console.error('Error in getAdultFoldersByUsername:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async createAdultFolder(req, res) { async createAdultFolder(req, res) {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;
@@ -267,8 +285,8 @@ class SocialNetworkController {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;
const { imageId } = req.params; const { imageId } = req.params;
const { title, visibilities } = req.body; const { title, visibilities, selectedUsers } = req.body;
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities); const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities, selectedUsers);
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId)); res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
} catch (error) { } catch (error) {
console.error('Error in changeAdultImage:', error); console.error('Error in changeAdultImage:', error);
@@ -287,6 +305,18 @@ class SocialNetworkController {
} }
} }
async getEroticVideosByUsername(req, res) {
try {
const userId = req.headers.userid;
const { username } = req.params;
const videos = await this.socialNetworkService.getEroticVideosByUsername(username, userId);
res.status(200).json(videos);
} catch (error) {
console.error('Error in getEroticVideosByUsername:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async uploadEroticVideo(req, res) { async uploadEroticVideo(req, res) {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;
@@ -300,6 +330,18 @@ class SocialNetworkController {
} }
} }
async changeEroticVideo(req, res) {
try {
const userId = req.headers.userid;
const { videoId } = req.params;
const updatedVideo = await this.socialNetworkService.changeEroticVideo(userId, videoId, req.body);
res.status(200).json(updatedVideo);
} catch (error) {
console.error('Error in changeEroticVideo:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async getEroticVideoByHash(req, res) { async getEroticVideoByHash(req, res) {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;

View File

@@ -18,15 +18,35 @@ class VocabController {
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 }); this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId)); this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query)); this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
this.getLanguageDictionary = this._wrapWithUser((userId, req) =>
this.service.getLanguageDictionary(userId, req.params.languageId, req.query)
);
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 });
this.getLessonVocabPool = this._wrapWithUser((userId, req) => this.service.getLessonVocabPool(userId, req.params.lessonId));
// Courses // Courses
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 }); 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.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.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
this.getCompletedLessonVocabPool = this._wrapWithUser((userId, req) =>
this.service.getCompletedLessonVocabPool(userId, req.params.courseId, req.query.untilLessonId)
);
this.getCourseDictionary = this._wrapWithUser((userId, req) =>
this.service.getCourseDictionary(userId, req.params.courseId, req.query)
);
this.getVocabDistractorPool = this._wrapWithUser((userId, req) =>
this.service.getVocabDistractorPool(userId, req.params.courseId, req.query.beforeLessonId)
);
this.getCourseSrsDue = this._wrapWithUser((userId, req) =>
this.service.getCourseSrsDue(userId, req.params.courseId, req.query)
);
this.reviewSrsItem = this._wrapWithUser((userId, req) =>
this.service.reviewSrsItem(userId, req.body),
{ successStatus: 201 }
);
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode)); 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.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)); this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
@@ -41,10 +61,12 @@ class VocabController {
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 }); 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.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId)); this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
this.getDashboardLearningSummary = this._wrapWithUser((userId) => this.service.getDashboardLearningSummary(userId));
// Progress // Progress
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId)); 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)); this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
this.resetLessonProgress = this._wrapWithUser((userId, req) => this.service.resetMyLessonProgress(userId, req.params.lessonId));
// Grammar Exercises // Grammar Exercises
this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes()); this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes());
@@ -77,4 +99,3 @@ class VocabController {
} }
export default VocabController; export default VocabController;

View File

@@ -0,0 +1,94 @@
/**
* Periodischer Job: reputation_periodic für politische Amtsinhaber.
* Aufruf: systemd-Timer oder FALUKANT_POLITICAL_REPUTATION_JOB=1 (siehe server.js).
*/
import PoliticalOffice from '../models/falukant/data/political_office.js';
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
import PoliticalOfficeBenefit from '../models/falukant/predefine/political_office_benefit.js';
import PoliticalOfficeBenefitType from '../models/falukant/type/political_office_benefit_type.js';
import PoliticalBenefitLastTick from '../models/falukant/data/political_benefit_last_tick.js';
import FalukantCharacter from '../models/falukant/data/character.js';
import FalukantUser from '../models/falukant/data/user.js';
import User from '../models/community/user.js';
import { sequelize } from '../utils/sequelize.js';
import { notifyUser } from '../utils/socket.js';
export async function runPoliticalReputationTicks() {
const offices = await PoliticalOffice.findAll({
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['id', 'name'] }]
});
const toNotify = new Set();
let ticks = 0;
for (const po of offices) {
const characterId = po.characterId;
const benefitRows = await PoliticalOfficeBenefit.findAll({
where: { officeTypeId: po.officeTypeId },
include: [
{
model: PoliticalOfficeBenefitType,
as: 'benefitDefinition',
attributes: ['tr'],
required: true,
where: { tr: 'reputation_periodic' }
}
]
});
for (const br of benefitRows) {
const v = br.value && typeof br.value === 'object' ? br.value : {};
const intervalDays = Math.max(1, Number(v.intervalDays ?? v.everyDays ?? 7));
const gain = Math.max(1, Number(v.gain ?? 1));
const [tickRow, created] = await PoliticalBenefitLastTick.findOrCreate({
where: {
characterId,
politicalOfficeBenefitId: br.id
},
defaults: {
characterId,
politicalOfficeBenefitId: br.id,
lastTickAt: new Date(po.createdAt),
ticksCount: 0
}
});
const baseMs = new Date(tickRow.lastTickAt).getTime();
const daysSince = Math.floor((Date.now() - baseMs) / 86400000);
if (daysSince < intervalDays) continue;
await sequelize.transaction(async (t) => {
const ch = await FalukantCharacter.findByPk(characterId, { transaction: t });
if (!ch) return;
const nextRep = Math.min(100, (ch.reputation ?? 0) + gain);
await ch.update({ reputation: nextRep }, { transaction: t });
await PoliticalBenefitLastTick.update(
{
lastTickAt: new Date(),
ticksCount: (tickRow.ticksCount || 0) + 1
},
{ where: { id: tickRow.id }, transaction: t }
);
});
ticks += 1;
toNotify.add(characterId);
console.info(
`[PoliticalBenefits] reputation_tick characterId=${characterId} benefitId=${br.id} gain=${gain}`
);
}
}
for (const characterId of toNotify) {
const ch = await FalukantCharacter.findByPk(characterId, { attributes: ['userId'] });
if (!ch?.userId) continue;
const fu = await FalukantUser.findOne({
where: { id: ch.userId },
include: [{ model: User, as: 'user', attributes: ['hashedId'] }]
});
const hid = fu?.user?.hashedId;
if (hid) notifyUser(hid, 'falukantUpdateStatus', {});
}
return { processedOffices: offices.length, ticksApplied: ticks };
}

View File

@@ -10,6 +10,9 @@ export const authenticate = async (req, res, next) => {
if (!user) { if (!user) {
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' }); return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
} }
if (!user.active) {
return res.status(403).json({ error: 'Unauthorized: User blocked' });
}
try { try {
await updateUserTimestamp(user.id); await updateUserTimestamp(user.id);
} catch (error) { } catch (error) {

View File

View File

@@ -0,0 +1,45 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
const table = { tableName: 'contact_message', schema: 'service' };
const columns = await queryInterface.describeTable(table);
if (!columns.answer) {
await queryInterface.addColumn(table, 'answer', {
type: Sequelize.TEXT,
allowNull: true
});
}
if (!columns.answered_at) {
await queryInterface.addColumn(table, 'answered_at', {
type: Sequelize.DATE,
allowNull: true
});
}
if (!columns.is_answered) {
await queryInterface.addColumn(table, 'is_answered', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
}
},
down: async (queryInterface, Sequelize) => {
const table = { tableName: 'contact_message', schema: 'service' };
const columns = await queryInterface.describeTable(table);
if (columns.answer) {
await queryInterface.removeColumn(table, 'answer');
}
if (columns.answered_at) {
await queryInterface.removeColumn(table, 'answered_at');
}
if (columns.is_answered) {
await queryInterface.removeColumn(table, 'is_answered');
}
}
};

View File

@@ -0,0 +1,89 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_image_visibility' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
visibility_type_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'type', tableName: 'image_visibility' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_visibility_user' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'user' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.sequelize.query(`
INSERT INTO community.erotic_video_image_visibility (erotic_video_id, visibility_type_id)
SELECT ev.id, iv.id
FROM community.erotic_video ev
CROSS JOIN type.image_visibility iv
WHERE iv.description = 'adults'
AND NOT EXISTS (
SELECT 1
FROM community.erotic_video_image_visibility eviv
WHERE eviv.erotic_video_id = ev.id
AND eviv.visibility_type_id = iv.id
)
`);
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_visibility_user' });
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_image_visibility' });
},
};

View File

@@ -0,0 +1,36 @@
"use strict";
/** Schwangerschaft (Admin / Spiel): erwarteter Geburtstermin + optionaler Vater-Charakter */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_due_at'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_due_at TIMESTAMPTZ NULL;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_father_character_id'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_father_character_id INTEGER NULL
REFERENCES falukant_data."character"(id) ON DELETE SET NULL;
END IF;
END$$;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_father_character_id;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_due_at;
`);
},
};

View File

@@ -0,0 +1,44 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS didactic_mode TEXT,
ADD COLUMN IF NOT EXISTS phase_label TEXT,
ADD COLUMN IF NOT EXISTS block_number INTEGER,
ADD COLUMN IF NOT EXISTS difficulty_weight INTEGER,
ADD COLUMN IF NOT EXISTS new_unit_target INTEGER,
ADD COLUMN IF NOT EXISTS review_weight INTEGER,
ADD COLUMN IF NOT EXISTS is_intensive_review BOOLEAN NOT NULL DEFAULT FALSE;
COMMENT ON COLUMN community.vocab_course_lesson.didactic_mode IS
'Didaktischer Modus der Lektion, z.B. core_input, guided_dialogue, intensive_review oder checkpoint.';
COMMENT ON COLUMN community.vocab_course_lesson.phase_label IS
'Übergeordnete Lernphase, z.B. quickstart, daily_life oder stabilization.';
COMMENT ON COLUMN community.vocab_course_lesson.block_number IS
'Inhaltlicher Block für Konsolidierungs- und Wiederholungswellen.';
COMMENT ON COLUMN community.vocab_course_lesson.difficulty_weight IS
'Grobe relative Schwierigkeit der Lektion von leicht bis schwer.';
COMMENT ON COLUMN community.vocab_course_lesson.new_unit_target IS
'Empfohlene Zahl neuer Spracheinheiten in dieser Lektion.';
COMMENT ON COLUMN community.vocab_course_lesson.review_weight IS
'Wie stark Wiederholung in dieser Lektion dominieren soll, typischerweise 0 bis 100.';
COMMENT ON COLUMN community.vocab_course_lesson.is_intensive_review IS
'Markiert Lektionen, die als intensive Wiederholungsphase gedacht sind.';
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS is_intensive_review,
DROP COLUMN IF EXISTS review_weight,
DROP COLUMN IF EXISTS new_unit_target,
DROP COLUMN IF EXISTS difficulty_weight,
DROP COLUMN IF EXISTS block_number,
DROP COLUMN IF EXISTS phase_label,
DROP COLUMN IF EXISTS didactic_mode;
`);
}
};

View File

@@ -0,0 +1,20 @@
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_progress
ADD COLUMN IF NOT EXISTS lesson_state JSONB NOT NULL DEFAULT '{}'::jsonb;
COMMENT ON COLUMN community.vocab_course_progress.lesson_state IS
'Persistierter UI- und Lernzustand pro Nutzer und Lektion fuer Resume im Sprachkurs.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_progress
DROP COLUMN IF EXISTS lesson_state;
`);
}
};

View File

@@ -0,0 +1,72 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on date NULL;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) THEN
ALTER TABLE falukant_predefine.political_office_benefit
RENAME COLUMN political_office_id TO office_type_id;
ELSIF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) THEN
UPDATE falukant_predefine.political_office_benefit
SET office_type_id = COALESCE(office_type_id, political_office_id);
ALTER TABLE falukant_predefine.political_office_benefit
DROP COLUMN political_office_id;
END IF;
END $$;
`);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
DROP COLUMN IF EXISTS last_political_daily_salary_on;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) THEN
ALTER TABLE falukant_predefine.political_office_benefit
RENAME COLUMN office_type_id TO political_office_id;
END IF;
END $$;
`);
}
};

View File

@@ -0,0 +1,30 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
INSERT INTO type.user_param_value (user_param_type_id, value, order_id)
SELECT upt.id, 'fr', COALESCE(
(SELECT MAX(v.order_id) FROM type.user_param_value v WHERE v.user_param_type_id = upt.id),
0
) + 1
FROM type.user_param upt
WHERE upt.description = 'language'
AND NOT EXISTS (
SELECT 1 FROM type.user_param_value x
WHERE x.user_param_type_id = upt.id AND x.value = 'fr'
);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DELETE FROM type.user_param_value v
USING type.user_param upt
WHERE v.user_param_type_id = upt.id
AND upt.description = 'language'
AND v.value = 'fr';
`);
},
};

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS certificate_productions_count_since TIMESTAMPTZ;
`);
await queryInterface.sequelize.query(`
COMMENT ON COLUMN falukant_data.falukant_user.certificate_productions_count_since IS
'Daemon/UI: Zählt nur falukant_log.production-Zeilen mit COALESCE(production_timestamp, production_date::timestamp) >= diesem Wert; bei Stufenänderung (Aufstieg/Bankrott/Erbfolge) auf NOW() (YpDaemon QUERY_UPDATE_FALUKANT_USER_CERTIFICATE). NULL = alle passenden Log-Zeilen bis zur ersten Stufenänderung nach Migration. Kein Löschen der Logs zum Reset.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
DROP COLUMN IF EXISTS certificate_productions_count_since;
`);
}
};

View File

@@ -0,0 +1,61 @@
'use strict';
/** @param {import('sequelize').QueryInterface} queryInterface */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.political_benefit_last_tick (
id serial PRIMARY KEY,
character_id integer NOT NULL
REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
political_office_benefit_id integer NOT NULL
REFERENCES falukant_predefine.political_office_benefit(id) ON DELETE CASCADE,
last_tick_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
ticks_count integer NOT NULL DEFAULT 0,
CONSTRAINT political_benefit_last_tick_unique UNIQUE (character_id, political_office_benefit_id)
);
CREATE INDEX IF NOT EXISTS political_benefit_last_tick_character_idx
ON falukant_data.political_benefit_last_tick (character_id);
`);
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.region_tax_history (
id serial PRIMARY KEY,
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
old_tax_percent numeric(12,4) NOT NULL,
new_tax_percent numeric(12,4) NOT NULL,
setter_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS region_tax_history_region_idx
ON falukant_data.region_tax_history (region_id, created_at DESC);
`);
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.political_appointment (
id serial PRIMARY KEY,
appointer_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
target_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
office_type_id integer NOT NULL REFERENCES falukant_type.political_office_type(id) ON DELETE CASCADE,
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
status varchar(32) NOT NULL DEFAULT 'completed',
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at timestamptz NULL,
completed_political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS political_appointment_appointer_idx
ON falukant_data.political_appointment (appointer_character_id, created_at DESC);
CREATE INDEX IF NOT EXISTS political_appointment_target_idx
ON falukant_data.political_appointment (target_character_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS falukant_data.political_appointment;
DROP TABLE IF EXISTS falukant_data.region_tax_history;
DROP TABLE IF EXISTS falukant_data.political_benefit_last_tick;
`);
}
};

View File

@@ -0,0 +1,50 @@
'use strict';
/** Stufe pro politischem Amt (Tageshonorar: base + perRank × hierarchy_level). */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.political_office_type
ADD COLUMN IF NOT EXISTS hierarchy_level INTEGER NOT NULL DEFAULT 1;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_type.political_office_type AS pot
SET hierarchy_level = sub.lvl
FROM (VALUES
('assessor', 1),
('councillor', 1),
('council', 2),
('beadle', 2),
('town-clerk', 2),
('mayor', 3),
('master-builder', 2),
('village-major', 2),
('judge', 3),
('bailif', 3),
('taxman', 2),
('sheriff', 3),
('consultant', 3),
('treasurer', 4),
('hangman', 2),
('territorial-council', 3),
('territorial-council-speaker', 4),
('ruler-consultant', 4),
('state-administrator', 4),
('super-state-administrator', 5),
('governor', 5),
('ministry-helper', 4),
('minister', 5),
('chancellor', 6)
) AS sub(name, lvl)
WHERE pot.name = sub.name;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.political_office_type
DROP COLUMN IF EXISTS hierarchy_level;
`);
}
};

View File

@@ -0,0 +1,32 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD COLUMN IF NOT EXISTS scandal_extra_daily_pct double precision NOT NULL DEFAULT 0;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD CONSTRAINT relationship_state_scandal_extra_daily_pct_chk
CHECK (scandal_extra_daily_pct >= 0 AND scandal_extra_daily_pct <= 100);
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP COLUMN IF EXISTS scandal_extra_daily_pct;
`);
},
};

View File

@@ -0,0 +1,54 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD COLUMN IF NOT EXISTS marriage_satisfaction integer;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_data.relationship_state
SET marriage_satisfaction = 55
WHERE marriage_satisfaction IS NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ALTER COLUMN marriage_satisfaction SET DEFAULT 55;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ALTER COLUMN marriage_satisfaction SET NOT NULL;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'relationship_state_marriage_satisfaction_check'
AND connamespace = 'falukant_data'::regnamespace
) THEN
ALTER TABLE falukant_data.relationship_state
ADD CONSTRAINT relationship_state_marriage_satisfaction_check
CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100);
END IF;
END $$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_marriage_satisfaction_check;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP COLUMN IF EXISTS marriage_satisfaction;
`);
}
};

View File

@@ -0,0 +1,49 @@
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_srs_item (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES community."user"(id) ON DELETE CASCADE,
course_id INTEGER NOT NULL REFERENCES community.vocab_course(id) ON DELETE CASCADE,
lesson_id INTEGER NULL REFERENCES community.vocab_course_lesson(id) ON DELETE SET NULL,
item_key VARCHAR(80) NOT NULL,
learning TEXT NOT NULL,
reference TEXT NOT NULL,
direction VARCHAR(8) NOT NULL DEFAULT 'BOTH',
stage INTEGER NOT NULL DEFAULT 0,
interval_days INTEGER NOT NULL DEFAULT 0,
last_reviewed_at TIMESTAMP WITH TIME ZONE NULL,
next_due_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
correct_count INTEGER NOT NULL DEFAULT 0,
wrong_count INTEGER NOT NULL DEFAULT 0,
lapse_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_srs_item_user_key_unique UNIQUE (user_id, item_key)
);
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_due
ON community.vocab_srs_item (user_id, course_id, next_due_at);
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_lesson
ON community.vocab_srs_item (user_id, course_id, lesson_id);
COMMENT ON TABLE community.vocab_srs_item IS
'Nutzerbezogener SRS-Fortschritt pro Vokabel/Phrase aus Sprachkursen.';
COMMENT ON COLUMN community.vocab_srs_item.item_key IS
'Stabiler deterministischer Schlüssel aus Kurs, Lektion und normalisiertem Begriffspaar.';
COMMENT ON COLUMN community.vocab_srs_item.stage IS
'SRS-Stufe. Höhere Stufen bedeuten längere Wiederholungsintervalle.';
COMMENT ON COLUMN community.vocab_srs_item.next_due_at IS
'Zeitpunkt, zu dem das Item wieder fällig ist.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS community.vocab_srs_item;
`);
}
};

View File

@@ -1,43 +0,0 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answer', {
type: Sequelize.TEXT,
allowNull: true
});
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answered_at', {
type: Sequelize.DATE,
allowNull: true
});
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'is_answered', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answer');
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answered_at');
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'is_answered');
}
};

View File

@@ -0,0 +1,27 @@
# Backend-Migrationen (Sequelize)
## Aktive Migrationen
Neue Schema-Änderungen: nur noch Dateien unter **`migrations-active/`**. Ausführung z.B.:
`npm run db:migrate` (siehe `backend/package.json`, Pfad `migrations-active`).
## Archiv
Ältere, bereits auf den Umgebungen ausgerollte Migrationen liegen unter **`migrations-archive/`** und werden vom Sequelize-CLI **nicht** mehr ausgeführt.
Vor dem ersten Deploy nach dieser Aufteilung: fehlende Einträge in `"SequelizeMeta"` für die archivierten Dateinamen per SQL ergänzen, siehe **`sql/baseline-sequelize-meta-migrations-archive.sql`** (idempotent).
## Sonstiges in diesem Ordner
SQL-Hilfsdateien und ältere Notizen verbleiben hier (werden nicht vom CLI geladen).
## Falukant: Zertifikat und Produktionszählung
| Datei (Archiv) | Inhalt |
|--------|--------|
| `migrations-archive/20260402140000-add-certificate-productions-count-since.cjs` | Spalte `falukant_data.falukant_user.certificate_productions_count_since` (`TIMESTAMPTZ`, nullable) inkl. Kommentar. Setzt die DB-Grundlage dafür, dass Daemon, Backend und UI dieselbe Periode für „abgeschlossene Produktionen“ nutzen (Filter mit `COALESCE(production_timestamp, production_date::timestamp)` ab diesem Zeitpunkt; `NULL` = bisherige Historie). |
Eine parallele SQL-Migration im Daemon-Repository (z.B. `014_falukant_certificate_productions_count_since.sql`) kann dieselbe Spalte anlegen, wenn das Deployment dort getrennt ist Schema doppelt anlegen vermeiden.
Details zur Zähl- und Retention-Logik: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md`.

View File

@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js'; import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.js'; import ImageImageVisibility from './community/image_image_visibility.js';
import FolderVisibilityUser from './community/folder_visibility_user.js'; import FolderVisibilityUser from './community/folder_visibility_user.js';
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
import GuestbookEntry from './community/guestbook.js'; import GuestbookEntry from './community/guestbook.js';
import Forum from './forum/forum.js'; import Forum from './forum/forum.js';
import Title from './forum/title.js'; import Title from './forum/title.js';
@@ -92,6 +94,9 @@ import Candidate from './falukant/data/candidate.js';
import Vote from './falukant/data/vote.js'; import Vote from './falukant/data/vote.js';
import PoliticalOfficeType from './falukant/type/political_office_type.js'; import PoliticalOfficeType from './falukant/type/political_office_type.js';
import PoliticalOffice from './falukant/data/political_office.js'; import PoliticalOffice from './falukant/data/political_office.js';
import PoliticalBenefitLastTick from './falukant/data/political_benefit_last_tick.js';
import RegionTaxHistory from './falukant/data/region_tax_history.js';
import PoliticalAppointment from './falukant/data/political_appointment.js';
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js'; import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js'; import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js'; import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
@@ -121,6 +126,7 @@ import VocabCourseProgress from './community/vocab_course_progress.js';
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js'; import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from './community/vocab_grammar_exercise.js'; import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js'; import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
import VocabSrsItem from './community/vocab_srs_item.js';
import CalendarEvent from './community/calendar_event.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';
@@ -242,6 +248,17 @@ export default function setupAssociations() {
otherKey: 'imageId' otherKey: 'imageId'
}); });
EroticVideo.belongsToMany(ImageVisibilityType, {
through: EroticVideoImageVisibility,
foreignKey: 'eroticVideoId',
otherKey: 'visibilityTypeId'
});
ImageVisibilityType.belongsToMany(EroticVideo, {
through: EroticVideoImageVisibility,
foreignKey: 'visibilityTypeId',
otherKey: 'eroticVideoId'
});
Folder.belongsToMany(ImageVisibilityUser, { Folder.belongsToMany(ImageVisibilityUser, {
through: FolderVisibilityUser, through: FolderVisibilityUser,
foreignKey: 'folderId', foreignKey: 'folderId',
@@ -253,6 +270,19 @@ export default function setupAssociations() {
otherKey: 'folderId' otherKey: 'folderId'
}); });
EroticVideo.belongsToMany(User, {
through: EroticVideoVisibilityUser,
foreignKey: 'eroticVideoId',
otherKey: 'userId',
as: 'selectedVisibilityUsers'
});
User.belongsToMany(EroticVideo, {
through: EroticVideoVisibilityUser,
foreignKey: 'userId',
otherKey: 'eroticVideoId',
as: 'visibleEroticVideos'
});
// Guestbook related associations // Guestbook related associations
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' }); User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' }); User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
@@ -353,6 +383,8 @@ export default function setupAssociations() {
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' }); FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' }); RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
FalukantCharacter.belongsTo(FalukantCharacter, { foreignKey: 'pregnancyFatherCharacterId', as: 'pregnancyFather' });
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' }); FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' }); FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
@@ -764,6 +796,48 @@ export default function setupAssociations() {
as: 'heldOffice' as: 'heldOffice'
}); });
PoliticalBenefitLastTick.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'character'
});
FalukantCharacter.hasMany(PoliticalBenefitLastTick, {
foreignKey: 'characterId',
as: 'politicalBenefitTicks'
});
PoliticalBenefitLastTick.belongsTo(PoliticalOfficeBenefit, {
foreignKey: 'politicalOfficeBenefitId',
as: 'officeBenefit'
});
RegionTaxHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(RegionTaxHistory, { foreignKey: 'regionId', as: 'taxHistory' });
RegionTaxHistory.belongsTo(FalukantCharacter, {
foreignKey: 'setterCharacterId',
as: 'setterCharacter'
});
RegionTaxHistory.belongsTo(PoliticalOffice, {
foreignKey: 'politicalOfficeId',
as: 'sourceOffice'
});
PoliticalAppointment.belongsTo(FalukantCharacter, {
foreignKey: 'appointerCharacterId',
as: 'appointer'
});
PoliticalAppointment.belongsTo(FalukantCharacter, {
foreignKey: 'targetCharacterId',
as: 'targetCharacter'
});
PoliticalAppointment.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
PoliticalAppointment.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
PoliticalAppointment.belongsTo(PoliticalOffice, {
foreignKey: 'completedPoliticalOfficeId',
as: 'completedOffice'
});
// elections // elections
Election.belongsTo(PoliticalOfficeType, { Election.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId', foreignKey: 'officeTypeId',
@@ -1103,6 +1177,13 @@ export default function setupAssociations() {
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' }); User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' }); VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' }); VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
VocabSrsItem.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabSrsItem, { foreignKey: 'userId', as: 'vocabSrsItems' });
VocabSrsItem.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabSrsItem, { foreignKey: 'courseId', as: 'srsItems' });
VocabSrsItem.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabSrsItem, { foreignKey: 'lessonId', as: 'srsItems' });
// Calendar associations // Calendar associations
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' }); CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });

View File

@@ -0,0 +1,26 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const EroticVideoImageVisibility = sequelize.define('erotic_video_image_visibility', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
eroticVideoId: {
type: DataTypes.INTEGER,
allowNull: false
},
visibilityTypeId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'erotic_video_image_visibility',
timestamps: false,
underscored: true,
schema: 'community'
});
export default EroticVideoImageVisibility;

View File

@@ -0,0 +1,26 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const EroticVideoVisibilityUser = sequelize.define('erotic_video_visibility_user', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
eroticVideoId: {
type: DataTypes.INTEGER,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'erotic_video_visibility_user',
timestamps: false,
underscored: true,
schema: 'community'
});
export default EroticVideoVisibilityUser;

View File

@@ -8,6 +8,26 @@ function encodeEncryptedValueToBlob(value) {
return Buffer.from(encrypted, 'utf8'); return Buffer.from(encrypted, 'utf8');
} }
/** Nur echte Adressen zurückgeben — verhindert Anzeige von Base64-/Key-artigem Müll bei fehlender Entschlüsselung. */
function looksLikePlausibleEmail(s) {
if (typeof s !== 'string') {
return false;
}
const t = s.trim();
if (!t || t.length > 254) {
return false;
}
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test(t);
}
function normalizeEmailCandidate(s) {
if (!s || typeof s !== 'string') {
return null;
}
const t = s.trim();
return looksLikePlausibleEmail(t) ? t : null;
}
function decodeEncryptedBlob(value) { function decodeEncryptedBlob(value) {
if (!value) { if (!value) {
return null; return null;
@@ -16,8 +36,9 @@ function decodeEncryptedBlob(value) {
try { try {
const encryptedUtf8 = value.toString('utf8'); const encryptedUtf8 = value.toString('utf8');
const decryptedUtf8 = decrypt(encryptedUtf8); const decryptedUtf8 = decrypt(encryptedUtf8);
if (decryptedUtf8) { const fromUtf8 = normalizeEmailCandidate(decryptedUtf8);
return decryptedUtf8; if (fromUtf8) {
return fromUtf8;
} }
} catch (error) { } catch (error) {
console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message); console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message);
@@ -26,15 +47,16 @@ function decodeEncryptedBlob(value) {
try { try {
const encryptedHex = value.toString('hex'); const encryptedHex = value.toString('hex');
const decryptedHex = decrypt(encryptedHex); const decryptedHex = decrypt(encryptedHex);
if (decryptedHex) { const fromHex = normalizeEmailCandidate(decryptedHex);
return decryptedHex; if (fromHex) {
return fromHex;
} }
} catch (error) { } catch (error) {
console.warn('Email legacy hex decryption failed:', error.message); console.warn('Email legacy hex decryption failed:', error.message);
} }
try { try {
return value.toString('utf8'); return normalizeEmailCandidate(value.toString('utf8'));
} catch (error) { } catch (error) {
console.warn('Email could not be read as plain text:', error.message); console.warn('Email could not be read as plain text:', error.message);
return null; return null;

View File

@@ -48,6 +48,42 @@ VocabCourseLesson.init({
defaultValue: 'vocab', defaultValue: 'vocab',
field: 'lesson_type' field: 'lesson_type'
}, },
didacticMode: {
type: DataTypes.TEXT,
allowNull: true,
field: 'didactic_mode'
},
phaseLabel: {
type: DataTypes.TEXT,
allowNull: true,
field: 'phase_label'
},
blockNumber: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'block_number'
},
difficultyWeight: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'difficulty_weight'
},
newUnitTarget: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'new_unit_target'
},
reviewWeight: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'review_weight'
},
isIntensiveReview: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'is_intensive_review'
},
audioUrl: { audioUrl: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true, allowNull: true,

View File

@@ -34,6 +34,12 @@ VocabCourseProgress.init({
allowNull: false, allowNull: false,
defaultValue: 0 defaultValue: 0
}, },
lessonState: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
field: 'lesson_state'
},
lastAccessedAt: { lastAccessedAt: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true, allowNull: true,

View File

@@ -0,0 +1,94 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabSrsItem extends Model {}
VocabSrsItem.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: true,
field: 'lesson_id'
},
itemKey: {
type: DataTypes.STRING(80),
allowNull: false,
field: 'item_key'
},
learning: {
type: DataTypes.TEXT,
allowNull: false
},
reference: {
type: DataTypes.TEXT,
allowNull: false
},
direction: {
type: DataTypes.STRING(8),
allowNull: false,
defaultValue: 'BOTH'
},
stage: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
intervalDays: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'interval_days'
},
lastReviewedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_reviewed_at'
},
nextDueAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'next_due_at'
},
correctCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'correct_count'
},
wrongCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'wrong_count'
},
lapseCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'lapse_count'
}
}, {
sequelize,
modelName: 'VocabSrsItem',
tableName: 'vocab_srs_item',
schema: 'community',
timestamps: true,
underscored: true
});
export default VocabSrsItem;

View File

@@ -45,6 +45,14 @@ FalukantCharacter.init(
min: 0, min: 0,
max: 100 max: 100
} }
},
pregnancyDueAt: {
type: DataTypes.DATE,
allowNull: true,
},
pregnancyFatherCharacterId: {
type: DataTypes.INTEGER,
allowNull: true,
} }
}, },
{ {
@@ -53,7 +61,12 @@ FalukantCharacter.init(
tableName: 'character', tableName: 'character',
schema: 'falukant_data', schema: 'falukant_data',
timestamps: true, timestamps: true,
underscored: true} underscored: true,
// Spalten erst nach Migration 20260330000000; ohne Exclude würde SELECT/INSERT auf alten DBs fehlschlagen
defaultScope: {
attributes: { exclude: ['pregnancyDueAt', 'pregnancyFatherCharacterId'] },
},
}
); );
export default FalukantCharacter; export default FalukantCharacter;

View File

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

View File

@@ -0,0 +1,56 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class PoliticalAppointment extends Model {}
PoliticalAppointment.init(
{
appointerCharacterId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'appointer_character_id'
},
targetCharacterId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'target_character_id'
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'office_type_id'
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'region_id'
},
status: {
type: DataTypes.STRING(32),
allowNull: false,
defaultValue: 'completed'
},
expiresAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'expires_at'
},
completedPoliticalOfficeId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'completed_political_office_id'
},
},
{
sequelize,
modelName: 'PoliticalAppointment',
tableName: 'political_appointment',
schema: 'falukant_data',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
underscored: true
}
);
export default PoliticalAppointment;

View File

@@ -0,0 +1,40 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class PoliticalBenefitLastTick extends Model {}
PoliticalBenefitLastTick.init(
{
characterId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'character_id'
},
politicalOfficeBenefitId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'political_office_benefit_id'
},
lastTickAt: {
type: DataTypes.DATE,
allowNull: false,
field: 'last_tick_at'
},
ticksCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'ticks_count'
}
},
{
sequelize,
modelName: 'PoliticalBenefitLastTick',
tableName: 'political_benefit_last_tick',
schema: 'falukant_data',
timestamps: false,
underscored: true
}
);
export default PoliticalBenefitLastTick;

View File

@@ -0,0 +1,46 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class RegionTaxHistory extends Model {}
RegionTaxHistory.init(
{
regionId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'region_id'
},
oldTaxPercent: {
type: DataTypes.DECIMAL(12, 4),
allowNull: false,
field: 'old_tax_percent'
},
newTaxPercent: {
type: DataTypes.DECIMAL(12, 4),
allowNull: false,
field: 'new_tax_percent'
},
setterCharacterId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'setter_character_id'
},
politicalOfficeId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'political_office_id'
}
},
{
sequelize,
modelName: 'RegionTaxHistory',
tableName: 'region_tax_history',
schema: 'falukant_data',
timestamps: true,
createdAt: 'created_at',
updatedAt: false,
underscored: true
}
);
export default RegionTaxHistory;

View File

@@ -88,6 +88,16 @@ RelationshipState.init(
min: 0, min: 0,
}, },
}, },
scandalExtraDailyPct: {
type: DataTypes.FLOAT,
allowNull: false,
defaultValue: 0,
validate: {
min: 0,
max: 100,
},
field: 'scandal_extra_daily_pct',
},
monthsUnderfunded: { monthsUnderfunded: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,

View File

@@ -29,6 +29,10 @@ FalukantUser.init({
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
defaultValue: 1}, defaultValue: 1},
certificateProductionsCountSince: {
type: DataTypes.DATE,
allowNull: true
},
mainBranchRegionId: { mainBranchRegionId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: true
@@ -36,6 +40,11 @@ FalukantUser.init({
lastNobilityAdvanceAt: { lastNobilityAdvanceAt: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true
},
lastPoliticalDailySalaryOn: {
type: DataTypes.DATEONLY,
allowNull: true,
field: 'last_political_daily_salary_on'
} }
}, { }, {
sequelize, sequelize,

View File

@@ -4,6 +4,11 @@ import { sequelize } from '../../../utils/sequelize.js';
class UserHouse extends Model { } class UserHouse extends Model { }
UserHouse.init({ UserHouse.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
roofCondition: { roofCondition: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,

View File

@@ -23,7 +23,11 @@ DayProduction.init({
productionDate: { productionDate: {
type: DataTypes.DATEONLY, type: DataTypes.DATEONLY,
allowNull: false, allowNull: false,
defaultValue: sequelize.literal('CURRENT_DATE')} defaultValue: sequelize.literal('CURRENT_DATE')},
completionCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1}
}, { }, {
sequelize, sequelize,
modelName: 'DayProduction', modelName: 'DayProduction',

View File

@@ -1,7 +1,6 @@
// falukant/predefine/political_office_benefit.js // falukant/predefine/political_office_benefit.js
import { Model, DataTypes } from 'sequelize'; import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js'; import { sequelize } from '../../../utils/sequelize.js';
import PoliticalOfficeBenefitType from '../type/political_office_benefit_type.js';
class PoliticalOfficeBenefit extends Model {} class PoliticalOfficeBenefit extends Model {}
@@ -9,31 +8,29 @@ PoliticalOfficeBenefit.init({
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true}, autoIncrement: true
politicalOfficeId: { },
officeTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false}, allowNull: false,
field: 'office_type_id'
},
benefitTypeId: { benefitTypeId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false}, allowNull: false,
field: 'benefit_type_id'
},
value: { value: {
type: DataTypes.JSONB, type: DataTypes.JSONB,
allowNull: false}}, { allowNull: false
sequelize, }
}, {
sequelize,
modelName: 'PoliticalOfficeBenefit', modelName: 'PoliticalOfficeBenefit',
tableName: 'political_office_benefit', tableName: 'political_office_benefit',
schema: 'falukant_predefine', schema: 'falukant_predefine',
timestamps: false, timestamps: false,
underscored: true}); underscored: true
// Association
PoliticalOfficeBenefit.belongsTo(PoliticalOfficeBenefitType, {
foreignKey: 'benefit_type_id',
as: 'benefitType'
});
PoliticalOfficeBenefitType.hasMany(PoliticalOfficeBenefit, {
foreignKey: 'benefit_type_id',
as: 'benefits'
}); });
export default PoliticalOfficeBenefit; export default PoliticalOfficeBenefit;

View File

@@ -17,10 +17,18 @@ PoliticalOfficeType.init({
regionType: { regionType: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false}, allowNull: false},
termLength: { termLength: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
defaultValue: 0}}, { defaultValue: 0},
/** Stufe für Tageshonorar (base + perRank × level) und Sortierung; 1 = niedrigstes politisches Level */
hierarchyLevel: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
field: 'hierarchy_level'
}
}, {
sequelize, sequelize,
modelName: 'PoliticalOfficeType', modelName: 'PoliticalOfficeType',
tableName: 'political_office_type', tableName: 'political_office_type',

View File

@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js'; import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.js'; import ImageImageVisibility from './community/image_image_visibility.js';
import FolderVisibilityUser from './community/folder_visibility_user.js'; import FolderVisibilityUser from './community/folder_visibility_user.js';
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
import GuestbookEntry from './community/guestbook.js'; import GuestbookEntry from './community/guestbook.js';
import DiaryHistory from './community/diary_history.js'; import DiaryHistory from './community/diary_history.js';
import Diary from './community/diary.js'; import Diary from './community/diary.js';
@@ -114,6 +116,9 @@ import PoliticalOfficeRequirement from './falukant/predefine/political_office_pr
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js'; import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js'; import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
import PoliticalOffice from './falukant/data/political_office.js'; import PoliticalOffice from './falukant/data/political_office.js';
import PoliticalBenefitLastTick from './falukant/data/political_benefit_last_tick.js';
import RegionTaxHistory from './falukant/data/region_tax_history.js';
import PoliticalAppointment from './falukant/data/political_appointment.js';
import Election from './falukant/data/election.js'; import Election from './falukant/data/election.js';
import Candidate from './falukant/data/candidate.js'; import Candidate from './falukant/data/candidate.js';
import Vote from './falukant/data/vote.js'; import Vote from './falukant/data/vote.js';
@@ -151,6 +156,7 @@ import VocabCourseProgress from './community/vocab_course_progress.js';
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js'; import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from './community/vocab_grammar_exercise.js'; import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js'; import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
import VocabSrsItem from './community/vocab_srs_item.js';
import CalendarEvent from './community/calendar_event.js'; import CalendarEvent from './community/calendar_event.js';
const models = { const models = {
@@ -179,6 +185,8 @@ const models = {
FolderImageVisibility, FolderImageVisibility,
ImageImageVisibility, ImageImageVisibility,
FolderVisibilityUser, FolderVisibilityUser,
EroticVideoImageVisibility,
EroticVideoVisibilityUser,
GuestbookEntry, GuestbookEntry,
DiaryHistory, DiaryHistory,
Diary, Diary,
@@ -258,6 +266,9 @@ const models = {
PoliticalOfficeBenefitType, PoliticalOfficeBenefitType,
PoliticalOfficeBenefit, PoliticalOfficeBenefit,
PoliticalOffice, PoliticalOffice,
PoliticalBenefitLastTick,
RegionTaxHistory,
PoliticalAppointment,
Election, Election,
Candidate, Candidate,
Vote, Vote,
@@ -308,6 +319,7 @@ const models = {
VocabGrammarExerciseType, VocabGrammarExerciseType,
VocabGrammarExercise, VocabGrammarExercise,
VocabGrammarExerciseProgress, VocabGrammarExerciseProgress,
VocabSrsItem,
// Calendar // Calendar
CalendarEvent, CalendarEvent,

View File

@@ -127,26 +127,29 @@ export async function createTriggers() {
const updateMoney = ` const updateMoney = `
CREATE OR REPLACE FUNCTION falukant_data.update_money( CREATE OR REPLACE FUNCTION falukant_data.update_money(
p_falukant_user_id integer, p_falukant_user_id integer,
p_money_change numeric, p_money_change numeric,
p_activity text, p_activity text,
p_changed_by integer DEFAULT NULL p_changed_by integer DEFAULT NULL::integer
) )
RETURNS void RETURNS void
LANGUAGE plpgsql LANGUAGE plpgsql
AS $function$ AS $function$
DECLARE DECLARE
v_money_before numeric(10,2); v_money_before numeric(14,2);
v_money_after numeric(10,2); v_money_after numeric(14,2);
v_moneyflow_id bigint; v_moneyflow_id bigint;
BEGIN BEGIN
SELECT money SELECT money
INTO v_money_before INTO v_money_before
FROM falukant_data.falukant_user FROM falukant_data.falukant_user
WHERE id = p_falukant_user_id; WHERE id = p_falukant_user_id;
IF NOT FOUND THEN IF NOT FOUND THEN
RAISE EXCEPTION 'FalukantUser mit ID % nicht gefunden', p_falukant_user_id; RAISE EXCEPTION 'FalukantUser mit ID % nicht gefunden', p_falukant_user_id;
END IF; END IF;
v_money_after := v_money_before + p_money_change; v_money_after := v_money_before + p_money_change;
INSERT INTO falukant_log.moneyflow ( INSERT INTO falukant_log.moneyflow (
falukant_user_id, falukant_user_id,
activity, activity,
@@ -160,22 +163,24 @@ export async function createTriggers() {
p_falukant_user_id, p_falukant_user_id,
p_activity, p_activity,
v_money_before, v_money_before,
NULL, -- Wird gleich aktualisiert NULL, -- wird gleich aktualisiert
p_money_change, p_money_change,
p_changed_by, p_changed_by,
NOW() NOW()
) )
RETURNING id INTO v_moneyflow_id; RETURNING id INTO v_moneyflow_id;
UPDATE falukant_data.falukant_user UPDATE falukant_data.falukant_user
SET money = v_money_after SET money = v_money_after
WHERE id = p_falukant_user_id; WHERE id = p_falukant_user_id;
UPDATE falukant_log.moneyflow UPDATE falukant_log.moneyflow
SET money_after = ( SET money_after = (
SELECT money SELECT money
FROM falukant_data.falukant_user FROM falukant_data.falukant_user
WHERE id = p_falukant_user_id WHERE id = p_falukant_user_id
) )
WHERE id = v_moneyflow_id; WHERE id = v_moneyflow_id;
END; END;
$function$; $function$;
`; `;

View File

@@ -1122,9 +1122,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.23", "version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/micromatch": { "node_modules/@types/micromatch": {
@@ -1244,9 +1244,9 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
@@ -1273,9 +1273,9 @@
} }
}, },
"node_modules/ansi-escapes": { "node_modules/ansi-escapes": {
"version": "7.2.0", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
"integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"environment": "^1.0.0" "environment": "^1.0.0"
@@ -1317,15 +1317,6 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/async": { "node_modules/async": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -1358,10 +1349,13 @@
} }
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"license": "MIT" "license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
}, },
"node_modules/base64id": { "node_modules/base64id": {
"version": "2.0.0", "version": "2.0.0",
@@ -1493,12 +1487,15 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.2", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
} }
}, },
"node_modules/braces": { "node_modules/braces": {
@@ -2156,9 +2153,9 @@
} }
}, },
"node_modules/dottie": { "node_modules/dottie": {
"version": "2.0.6", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/draco3dgltf": { "node_modules/draco3dgltf": {
@@ -2188,15 +2185,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/editorconfig": { "node_modules/editorconfig": {
"version": "1.0.4", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@one-ini/wasm": "0.1.1", "@one-ini/wasm": "0.1.1",
"commander": "^10.0.0", "commander": "^10.0.0",
"minimatch": "9.0.1", "minimatch": "^9.0.1",
"semver": "^7.5.3" "semver": "^7.5.3"
}, },
"bin": { "bin": {
@@ -2808,9 +2805,9 @@
} }
}, },
"node_modules/get-east-asian-width": { "node_modules/get-east-asian-width": {
"version": "1.4.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -2876,21 +2873,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gltf-validator": { "node_modules/gltf-validator": {
"version": "2.0.0-dev.3.10", "version": "2.0.0-dev.3.10",
"resolved": "https://registry.npmjs.org/gltf-validator/-/gltf-validator-2.0.0-dev.3.10.tgz", "resolved": "https://registry.npmjs.org/gltf-validator/-/gltf-validator-2.0.0-dev.3.10.tgz",
@@ -3404,9 +3386,9 @@
} }
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/log-update": { "node_modules/log-update": {
@@ -3668,16 +3650,15 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.1", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true, "license": "BlueOak-1.0.0",
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^5.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": "18 || 20 || >=22"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -3876,9 +3857,9 @@
} }
}, },
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "8.0.3", "version": "8.0.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
"integrity": "sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==", "integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
"license": "MIT-0", "license": "MIT-0",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -4168,9 +4149,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
@@ -4232,9 +4213,9 @@
} }
}, },
"node_modules/property-graph": { "node_modules/property-graph": {
"version": "4.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.0.0.tgz", "resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz",
"integrity": "sha512-I0hojAJfTbSCZy3y6xyK29eayxo14v1bj1VPiDkHjTdz33SV6RdfMz2AHnf4ai62Vng2mN5GkaKahkooBIo9gA==", "integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/proto-list": { "node_modules/proto-list": {
@@ -5009,22 +4990,22 @@
} }
}, },
"node_modules/socket.io-parser": { "node_modules/socket.io-parser": {
"version": "4.2.4", "version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1" "debug": "~4.4.1"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/socket.io-parser/node_modules/debug": { "node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@@ -5298,6 +5279,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/table/node_modules/astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/table/node_modules/color-convert": { "node_modules/table/node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -5513,9 +5503,9 @@
} }
}, },
"node_modules/underscore": { "node_modules/underscore": {
"version": "1.13.7", "version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": { "node_modules/undici": {

View File

@@ -9,11 +9,13 @@
"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",
"db:migrate": "sequelize-cli db:migrate --config config/sequelize-cli.cjs --migrations-path migrations-active --env production",
"sync-tables": "node sync-tables-only.js", "sync-tables": "node sync-tables-only.js",
"check-connections": "node check-connections.js", "check-connections": "node check-connections.js",
"cleanup-connections": "node cleanup-connections.js", "cleanup-connections": "node cleanup-connections.js",
"diag:town-worth": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-town-product-worth-stats.mjs", "diag:town-worth": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-town-product-worth-stats.mjs",
"diag:moneyflow": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-moneyflow-report.mjs", "diag:moneyflow": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-moneyflow-report.mjs",
"sync:vocab-courses": "node scripts/sync-vocab-course-content.js",
"lockfile:sync": "npm install --package-lock-only", "lockfile:sync": "npm install --package-lock-only",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
@@ -21,6 +23,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@gltf-transform/cli": "^4.3.0",
"amqplib": "^0.10.9", "amqplib": "^0.10.9",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"connect-redis": "^9.0.0", "connect-redis": "^9.0.0",
@@ -43,10 +46,12 @@
"sharp": "^0.34.5", "sharp": "^0.34.5",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"uuid": "^13.0.0", "uuid": "^13.0.0",
"ws": "^8.20.0", "ws": "^8.20.0"
"@gltf-transform/cli": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"sequelize-cli": "^6.6.5" "sequelize-cli": "^6.6.5"
},
"overrides": {
"minimatch": "10.2.4"
} }
} }

View File

@@ -2,6 +2,7 @@
import { Router } from 'express'; import { Router } from 'express';
import { authenticate } from '../middleware/authMiddleware.js'; import { authenticate } from '../middleware/authMiddleware.js';
import AdminController from '../controllers/adminController.js'; import AdminController from '../controllers/adminController.js';
import moderationController from '../controllers/moderationController.js';
const router = Router(); const router = Router();
const adminController = new AdminController(); const adminController = new AdminController();
@@ -25,6 +26,14 @@ router.put('/users/:id/adult-verification', authenticate, adminController.setAdu
router.get('/users/erotic-moderation', authenticate, adminController.getEroticModerationReports); router.get('/users/erotic-moderation', authenticate, adminController.getEroticModerationReports);
router.get('/users/erotic-moderation/preview/:type/:targetId', authenticate, adminController.getEroticModerationPreview); router.get('/users/erotic-moderation/preview/:type/:targetId', authenticate, adminController.getEroticModerationPreview);
router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction); router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction);
router.get('/users/:id/vocab-courses', authenticate, adminController.getUserVocabCourses);
router.post('/users/:id/vocab-lesson-progress/reset', authenticate, adminController.resetUserVocabLessonProgress);
router.post(
'/users/:id/vocab-lesson-progress/mark-complete-through',
authenticate,
adminController.markUserVocabLessonsCompleteThrough
);
router.get('/vocab/courses/:courseId', authenticate, adminController.getVocabCourseForAdmin);
router.get('/users/:id', authenticate, adminController.getUser); router.get('/users/:id', authenticate, adminController.getUser);
router.put('/users/:id', authenticate, adminController.updateUser); router.put('/users/:id', authenticate, adminController.updateUser);
@@ -43,15 +52,26 @@ router.post('/contacts/answer', authenticate, adminController.answerContact);
router.post('/falukant/searchuser', authenticate, adminController.searchUser); router.post('/falukant/searchuser', authenticate, adminController.searchUser);
router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById); router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById);
router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser); router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser);
router.get('/falukant/character/:characterId/potential-fathers', authenticate, adminController.adminGetPotentialFathersForCharacter);
router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy);
router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth);
router.post('/falukant/character/:characterId/death-cleanup', authenticate, adminController.adminCleanupCharacterDeathArtifacts);
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches); router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock); router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
router.post('/falukant/stock', authenticate, adminController.addFalukantStock); router.post('/falukant/stock', authenticate, adminController.addFalukantStock);
router.get('/falukant/stock-types', authenticate, adminController.getFalukantStockTypes); router.get('/falukant/stock-types', authenticate, adminController.getFalukantStockTypes);
router.get('/falukant/region-types', authenticate, adminController.getFalukantRegionTypes);
router.get('/falukant/regions', authenticate, adminController.getFalukantRegions); router.get('/falukant/regions', authenticate, adminController.getFalukantRegions);
router.get('/falukant/regions/all', authenticate, adminController.getFalukantAllRegions);
router.post('/falukant/regions', authenticate, adminController.createFalukantRegion);
router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap); router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalukantRegionMap);
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances); router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance); router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance); router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
router.get('/moderation/reports', authenticate, moderationController.listReports);
router.get('/moderation/reports/open-count', authenticate, moderationController.getOpenReportCount);
router.post('/moderation/reports/:reportId/status', authenticate, moderationController.updateReportStatus);
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs); router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus); router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility); router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);

View File

@@ -18,5 +18,6 @@ router.get('/rooms', authenticate, chatController.getRoomList);
router.get('/room-create-options', authenticate, chatController.getRoomCreateOptions); router.get('/room-create-options', authenticate, chatController.getRoomCreateOptions);
router.get('/my-rooms', authenticate, chatController.getOwnRooms); router.get('/my-rooms', authenticate, chatController.getOwnRooms);
router.delete('/my-rooms/:id', authenticate, chatController.deleteOwnRoom); router.delete('/my-rooms/:id', authenticate, chatController.deleteOwnRoom);
router.post('/report', chatController.reportChatIncident);
export default router; export default router;

View File

@@ -22,10 +22,12 @@ router.post('/production', falukantController.createProduction);
router.get('/production/:branchId', falukantController.getProduction); router.get('/production/:branchId', falukantController.getProduction);
router.get('/stocktypes', falukantController.getStockTypes); router.get('/stocktypes', falukantController.getStockTypes);
router.get('/stockoverview', falukantController.getStockOverview); router.get('/stockoverview', falukantController.getStockOverview);
router.get('/stock/?:branchId', falukantController.getStock); router.get('/stock', falukantController.getStock);
router.get('/stock/:branchId', falukantController.getStock);
router.post('/stock', falukantController.createStock); router.post('/stock', falukantController.createStock);
router.get('/products', falukantController.getProducts); router.get('/products', falukantController.getProducts);
router.get('/inventory/?:branchId', falukantController.getInventory); router.get('/inventory', falukantController.getInventory);
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);
@@ -52,6 +54,7 @@ router.post('/family/marriage/spend-time', falukantController.spendTimeWithSpous
router.post('/family/marriage/gift', falukantController.giftToSpouse); router.post('/family/marriage/gift', falukantController.giftToSpouse);
router.post('/family/marriage/reconcile', falukantController.reconcileMarriage); router.post('/family/marriage/reconcile', falukantController.reconcileMarriage);
router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance); router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance);
router.post('/family/lover/:relationshipId/improve-affection', falukantController.improveLoverAffection);
router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover); router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover);
router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship); router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship);
router.get('/heirs/potential', falukantController.getPotentialHeirs); router.get('/heirs/potential', falukantController.getPotentialHeirs);
@@ -93,6 +96,13 @@ router.post('/nobility', falukantController.advanceNobility);
router.get('/health', falukantController.getHealth); router.get('/health', falukantController.getHealth);
router.post('/health', falukantController.healthActivity); router.post('/health', falukantController.healthActivity);
router.get('/politics/overview', falukantController.getPoliticsOverview); router.get('/politics/overview', falukantController.getPoliticsOverview);
router.get('/politics/offices', falukantController.getPoliticalOfficeCatalog);
router.get('/politics/my-powers', falukantController.getPoliticalMyPowers);
router.get('/politics/tax-jurisdiction', falukantController.getPoliticalTaxJurisdiction);
router.put('/politics/region/:regionId/tax', falukantController.setPoliticalRegionTax);
router.get('/politics/region/:regionId/tax-history', falukantController.getPoliticalRegionTaxHistory);
router.get('/politics/appointable-offices', falukantController.getPoliticalAppointableOffices);
router.post('/politics/appointments', falukantController.createPoliticalAppointment);
router.get('/politics/open', falukantController.getOpenPolitics); router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections); router.post('/politics/open', falukantController.applyForElections);
router.get('/politics/elections', falukantController.getElections); router.get('/politics/elections', falukantController.getElections);

View File

@@ -0,0 +1,10 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import moderationController from '../controllers/moderationController.js';
const router = express.Router();
router.use(authenticate);
router.post('/reports', moderationController.createReport);
export default router;

View File

@@ -17,12 +17,15 @@ router.get('/folder/:folderId', socialNetworkController.getFolderImageList);
router.post('/images', upload.single('image'), socialNetworkController.uploadImage); router.post('/images', upload.single('image'), socialNetworkController.uploadImage);
router.post('/erotic/folders/:folderId', socialNetworkController.createAdultFolder); router.post('/erotic/folders/:folderId', socialNetworkController.createAdultFolder);
router.get('/erotic/folders', socialNetworkController.getAdultFolders); router.get('/erotic/folders', socialNetworkController.getAdultFolders);
router.get('/profile/erotic/folders/:username', socialNetworkController.getAdultFoldersByUsername);
router.get('/profile/erotic/videos/:username', socialNetworkController.getEroticVideosByUsername);
router.get('/erotic/folder/:folderId', socialNetworkController.getAdultFolderImageList); router.get('/erotic/folder/:folderId', socialNetworkController.getAdultFolderImageList);
router.post('/erotic/images', upload.single('image'), socialNetworkController.uploadAdultImage); router.post('/erotic/images', upload.single('image'), socialNetworkController.uploadAdultImage);
router.put('/erotic/images/:imageId', socialNetworkController.changeAdultImage); router.put('/erotic/images/:imageId', socialNetworkController.changeAdultImage);
router.get('/erotic/image/:hash', socialNetworkController.getAdultImageByHash); router.get('/erotic/image/:hash', socialNetworkController.getAdultImageByHash);
router.get('/erotic/videos', socialNetworkController.listEroticVideos); router.get('/erotic/videos', socialNetworkController.listEroticVideos);
router.post('/erotic/videos', upload.single('video'), socialNetworkController.uploadEroticVideo); router.post('/erotic/videos', upload.single('video'), socialNetworkController.uploadEroticVideo);
router.put('/erotic/videos/:videoId', socialNetworkController.changeEroticVideo);
router.get('/erotic/video/:hash', socialNetworkController.getEroticVideoByHash); router.get('/erotic/video/:hash', socialNetworkController.getEroticVideoByHash);
router.post('/erotic/report', socialNetworkController.reportEroticContent); router.post('/erotic/report', socialNetworkController.reportEroticContent);
router.get('/images/:imageId', socialNetworkController.getImage); router.get('/images/:imageId', socialNetworkController.getImage);

View File

@@ -7,6 +7,8 @@ const vocabController = new VocabController();
router.use(authenticate); router.use(authenticate);
router.get('/dashboard-widget', vocabController.getDashboardLearningSummary);
router.get('/languages', vocabController.listLanguages); router.get('/languages', vocabController.listLanguages);
router.get('/languages/all', vocabController.listAllLanguages); router.get('/languages/all', vocabController.listAllLanguages);
router.post('/languages', vocabController.createLanguage); router.post('/languages', vocabController.createLanguage);
@@ -18,19 +20,26 @@ router.get('/languages/:languageId/chapters', vocabController.listChapters);
router.post('/languages/:languageId/chapters', vocabController.createChapter); router.post('/languages/:languageId/chapters', vocabController.createChapter);
router.get('/languages/:languageId/vocabs', vocabController.listLanguageVocabs); router.get('/languages/:languageId/vocabs', vocabController.listLanguageVocabs);
router.get('/languages/:languageId/search', vocabController.searchVocabs); router.get('/languages/:languageId/search', vocabController.searchVocabs);
router.get('/languages/:languageId/dictionary', vocabController.getLanguageDictionary);
router.get('/chapters/:chapterId', vocabController.getChapter); 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);
router.get('/lessons/:lessonId/vocab-pool', vocabController.getLessonVocabPool);
// Courses // Courses
router.post('/courses', vocabController.createCourse); router.post('/courses', vocabController.createCourse);
router.get('/courses', vocabController.getCourses); router.get('/courses', vocabController.getCourses);
router.get('/courses/my', vocabController.getMyCourses); router.get('/courses/my', vocabController.getMyCourses);
router.post('/courses/find-by-code', vocabController.getCourseByShareCode); router.post('/courses/find-by-code', vocabController.getCourseByShareCode);
router.get('/courses/:courseId/completed-lesson-vocabs', vocabController.getCompletedLessonVocabPool);
router.get('/courses/:courseId/dictionary', vocabController.getCourseDictionary);
router.get('/courses/:courseId/distractor-pool', vocabController.getVocabDistractorPool);
router.get('/courses/:courseId/srs/due', vocabController.getCourseSrsDue);
router.get('/courses/:courseId', vocabController.getCourse); router.get('/courses/:courseId', vocabController.getCourse);
router.put('/courses/:courseId', vocabController.updateCourse); router.put('/courses/:courseId', vocabController.updateCourse);
router.delete('/courses/:courseId', vocabController.deleteCourse); router.delete('/courses/:courseId', vocabController.deleteCourse);
router.post('/srs/review', vocabController.reviewSrsItem);
// Lessons // Lessons
router.post('/courses/:courseId/lessons', vocabController.addLessonToCourse); router.post('/courses/:courseId/lessons', vocabController.addLessonToCourse);
@@ -45,6 +54,7 @@ router.delete('/courses/:courseId/enroll', vocabController.unenrollFromCourse);
router.get('/courses/:courseId/progress', vocabController.getCourseProgress); router.get('/courses/:courseId/progress', vocabController.getCourseProgress);
router.get('/lessons/:lessonId', vocabController.getLesson); router.get('/lessons/:lessonId', vocabController.getLesson);
router.put('/lessons/:lessonId/progress', vocabController.updateLessonProgress); router.put('/lessons/:lessonId/progress', vocabController.updateLessonProgress);
router.delete('/lessons/:lessonId/progress', vocabController.resetLessonProgress);
// Grammar Exercises // Grammar Exercises
router.get('/grammar/exercise-types', vocabController.getExerciseTypes); router.get('/grammar/exercise-types', vocabController.getExerciseTypes);
@@ -58,4 +68,3 @@ router.put('/grammar-exercises/:exerciseId', vocabController.updateGrammarExerci
router.delete('/grammar-exercises/:exerciseId', vocabController.deleteGrammarExercise); router.delete('/grammar-exercises/:exerciseId', vocabController.deleteGrammarExercise);
export default router; export default router;

View File

@@ -9,6 +9,7 @@
import { sequelize } from '../utils/sequelize.js'; import { sequelize } from '../utils/sequelize.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js'; import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js';
const LESSONS_TO_ADD = [ const LESSONS_TO_ADD = [
{ {
@@ -98,6 +99,8 @@ async function addBisayaWeek1Lessons() {
continue; continue;
} }
const pedagogy = getBisayaLessonPedagogy(lessonData.lessonNumber) || {};
await VocabCourseLesson.create({ await VocabCourseLesson.create({
courseId: course.id, courseId: course.id,
chapterId: null, chapterId: null,
@@ -113,7 +116,14 @@ async function addBisayaWeek1Lessons() {
speakingPrompts: lessonData.speakingPrompts || [], speakingPrompts: lessonData.speakingPrompts || [],
targetMinutes: lessonData.targetMinutes, targetMinutes: lessonData.targetMinutes,
targetScorePercent: lessonData.targetScorePercent, targetScorePercent: lessonData.targetScorePercent,
requiresReview: lessonData.requiresReview requiresReview: lessonData.requiresReview,
didacticMode: pedagogy.didacticMode || null,
phaseLabel: pedagogy.phaseLabel || null,
blockNumber: pedagogy.blockNumber ?? null,
difficultyWeight: pedagogy.difficultyWeight ?? null,
newUnitTarget: pedagogy.newUnitTarget ?? null,
reviewWeight: pedagogy.reviewWeight ?? null,
isIntensiveReview: Boolean(pedagogy.isIntensiveReview)
}); });
console.log(` ✅ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" hinzugefügt`); console.log(` ✅ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" hinzugefügt`);

View File

@@ -12,6 +12,12 @@ import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js'
import VocabCourseProgress from '../models/community/vocab_course_progress.js'; import VocabCourseProgress from '../models/community/vocab_course_progress.js';
import VocabGrammarExerciseProgress from '../models/community/vocab_grammar_exercise_progress.js'; import VocabGrammarExerciseProgress from '../models/community/vocab_grammar_exercise_progress.js';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import { getBisayaLessonPedagogy } from './bisaya-course-phase2-pedagogy.js';
import {
BISAYA_DIDACTICS_24_43,
BISAYA_LESSONS_24_43_BY_NUMBER,
BISAYA_RELATIONSHIP_ANCHOR_DIDACTICS
} from './bisaya-course-plan-24-43.js';
const LESSON_DIDACTICS = { const LESSON_DIDACTICS = {
'Begrüßungen & Höflichkeit': { 'Begrüßungen & Höflichkeit': {
@@ -21,31 +27,158 @@ const LESSON_DIDACTICS = {
'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.', 'Höfliche Reaktionen wie Danke und Bitte passend einsetzen.',
'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.' 'Ein kurzes Begrüßungs-Mini-Gespräch laut üben.'
], ],
corePatterns: ['Kumusta ka?', 'Maayong buntag.', 'Maayong adlaw.', 'Amping.', 'Babay.', 'Maayo ko.', 'Salamat.', 'Palihug.'], corePatterns: [
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
{ target: 'Maayong buntag.', gloss: 'Guten Morgen.' },
{ target: 'Maayong adlaw.', gloss: 'Guten Tag.' },
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
{ target: 'Maayong gabii, matulog na ta.', gloss: 'Guten Abend, wir legen uns schlafen.' },
{ target: 'Katulog og maayo.', gloss: 'Schlaf gut.' },
{ target: 'Kapoy na ka?', gloss: 'Bist du müde?' },
{ target: 'Matulog na ta.', gloss: 'Lass uns schlafen gehen.' },
{ target: 'Inom sa og tubig.', gloss: 'Trink Wasser.' },
{ target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' },
{ target: 'Tabuni ang imong kaugalingon.', gloss: 'Deck dich zu.' },
{ target: 'Ugma nasad.', gloss: 'Bis morgen wieder.' },
{ target: 'Damgo og nindot.', gloss: 'Träum schön.' },
{ target: 'Amping.', gloss: 'Pass auf dich auf.' },
{ target: 'Babay.', gloss: 'Tschüss.' },
{ target: 'Maayo ko.', gloss: 'Mir geht es gut.' },
{ target: 'Salamat.', gloss: 'Danke.' },
{ target: 'Palihug.', gloss: 'Bitte.' }
],
grammarFocus: [ grammarFocus: [
{ title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' }, { title: 'Kurzantworten mit ko', text: 'Mit "ko" sprichst du über dich selbst: "Maayo ko."', example: 'Maayo ko. = Mir geht es gut.' },
{ title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' } { title: 'Maayong + Tageszeit', text: 'Mit "Maayong" kannst du Grüße für verschiedene Tageszeiten bilden.', example: 'Maayong buntag. / Maayong gabii.' },
{ title: 'Abend und Schlafen', text: 'Im Familienalltag folgen auf einen Abendgruß oft direkte Schlafens- oder Fürsorgeformeln.', example: 'Maayong gabii. / Katulog og maayo.' },
{ title: 'Sanfte Schlafensroutine', text: 'Kurze Fragen nach Müdigkeit und kleine Aufforderungen klingen im Familienalltag natürlicher als lange Sätze.', example: 'Kapoy na ka? Matulog na ta. Inom sa og tubig.' },
{ title: 'Familiäre Abendformeln', text: 'Am Abend folgen oft sehr kurze Handlungsformeln wie Licht aus, zudecken oder ein Schlafwunsch.', example: 'Patya ang suga. Tabuni ang imong kaugalingon. Damgo og nindot.' }
], ],
speakingPrompts: [ speakingPrompts: [
{ title: 'Mini-Gespräch', prompt: 'Begrüße eine Person, frage nach dem Befinden und reagiere höflich.', cue: 'Kumusta ka? Maayo ko. Salamat.' }, { title: 'Mini-Gespräch', prompt: 'Begrüße eine Person, frage nach dem Befinden und reagiere höflich.', cue: 'Kumusta ka? Maayo ko. Salamat.' },
{ title: 'Verabschiedung', prompt: 'Verabschiede dich kurz und wünsche, dass die andere Person auf sich aufpasst.', cue: 'Babay. Amping.' } { title: 'Verabschiedung', prompt: 'Verabschiede dich kurz und wünsche, dass die andere Person auf sich aufpasst.', cue: 'Babay. Amping.' },
{ title: 'Abend und Schlaf', prompt: 'Wünsche einen guten Abend, eine gute Nacht und dass die Person gut schlafen soll.', cue: 'Maayong gabii. Katulog og maayo.' },
{ title: 'Schlafensroutine', prompt: 'Frage, ob die Person müde ist, und leite dann sanft zum Schlafengehen über.', cue: 'Kapoy na ka? Matulog na ta. Inom sa og tubig.' },
{ title: 'Vor dem Schlafen', prompt: 'Bitte darum, das Licht auszumachen, sich zuzudecken, und wünsche eine gute Nacht bis morgen.', cue: 'Patya ang suga. Tabuni ang imong kaugalingon. Ugma nasad. Damgo og nindot.' }
], ],
practicalTasks: [{ title: 'Alltag', text: 'Sprich die Begrüßung dreimal laut und variiere die Antwort.' }] practicalTasks: [{ title: 'Alltag', text: 'Sprich die Begrüßung dreimal laut und variiere die Antwort.' }]
}, },
'Familienwörter': { 'Familienwörter': {
learningGoals: [ learningGoals: [
'Die wichtigsten Familienbezeichnungen sicher erkennen.', 'Die wichtigsten Familienbezeichnungen sicher erkennen.',
'Familienmitglieder mit respektvollen Wörtern ansprechen.', 'Familienmitglieder und Großeltern mit respektvollen Wörtern ansprechen.',
'Kurze Sätze über die eigene Familie bilden.' 'Kurze Sätze über die eigene Familie bilden.'
], ],
corePatterns: ['Si Nanay', 'Si Tatay', 'Kuya nako', 'Ate nako'], corePatterns: [
{ target: 'Si Nanay.', gloss: 'Das ist Mama.' },
{ target: 'Si Tatay.', gloss: 'Das ist Papa.' },
{ target: 'Si Kuya nako.', gloss: 'Das ist mein älterer Bruder.' },
{ target: 'Si Ate nako.', gloss: 'Das ist meine ältere Schwester.' },
{ target: 'Si Dodong nako.', gloss: 'Das ist mein jüngerer Bruder.' },
{ target: 'Si Inday nako.', gloss: 'Das ist meine jüngere Schwester.' },
{ target: 'Si Lola nako.', gloss: 'Das ist meine Großmutter.' },
{ target: 'Si Lolo nako.', gloss: 'Das ist mein Großvater.' }
],
grammarFocus: [ grammarFocus: [
{ title: 'Respekt in Familienanreden', text: 'Kuya und Ate werden nicht nur in der Familie, sondern auch respektvoll für ältere Personen benutzt.', example: 'Kuya, palihug.' } { title: 'Respekt in Familienanreden', text: 'Kuya und Ate richtest du an ältere Geschwister (oder respektvoll an andere). Dodong und Inday nutzt du für jüngere Brüder bzw. Schwestern; „Ading“ ist eine weiche Anrede an jüngere Geschwister.', example: 'Kuya, palihug. / Si Dodong nako.' },
{ title: 'si als Personenmarker', text: 'Mit "si" markierst du im einfachen Satz eine konkrete Person.', example: 'Si Nanay. Si Tatay.' }
], ],
speakingPrompts: [ speakingPrompts: [
{ title: 'Meine Familie', prompt: 'Stelle zwei Familienmitglieder mit einem kurzen Satz vor.', cue: 'Si Nanay. Si Kuya.' } { title: 'Meine Familie', prompt: 'Stelle vier Familienmitglieder mit kurzen Sätzen vor.', cue: 'Si Nanay. Si Tatay. Si Kuya nako. Si Dodong nako.' }
], ],
practicalTasks: [{ title: 'Familienpraxis', text: 'Nenne laut fünf Familienwörter und bilde danach zwei Mini-Sätze.' }] practicalTasks: [{ title: 'Familienpraxis', text: 'Nenne laut die acht Kern-Familienwörter und bilde danach drei Mini-Sätze über deine Familie.' }]
},
'Überlebenssätze - Teil 1': {
learningGoals: [
'Zentrale Notfall- und Verständnisfragen schnell abrufen.',
'Höflich um Wiederholung, Hilfe und langsamere Sprache bitten.',
'Drei Überlebenssätze hintereinander sicher sprechen.'
],
corePatterns: [
{ target: 'Wala ko kasabot.', gloss: 'Ich verstehe nicht.' },
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
{ target: 'Asa ang CR?', gloss: 'Wo ist die Toilette?' },
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' },
{ target: 'Tabangi ko, palihug.', gloss: 'Hilf mir bitte.' },
{ target: 'Unsay pasabot ani?', gloss: 'Was bedeutet das?' }
],
grammarFocus: [
{ title: 'Bitte-Formeln mit palihug', text: '"Palihug" macht Bitten höflich und taucht in vielen Überlebenssätzen auf.', example: 'Palihug ka mubalik? / Tabangi ko, palihug.' },
{ title: 'Kurze Verständnisfragen', text: 'Sehr kurze Fragen helfen dir im Alltag oft mehr als lange Sätze.', example: 'Unsay pasabot ani? Asa ang CR?' }
],
speakingPrompts: [
{ title: 'Wenn du etwas nicht verstehst', prompt: 'Sage, dass du etwas nicht verstehst, und bitte um Wiederholung.', cue: 'Wala ko kasabot. Palihug ka mubalik?' },
{ title: 'Soforthilfe', prompt: 'Bitte um Hilfe und frage dann nach der Toilette oder nach der Bedeutung eines Wortes.', cue: 'Tabangi ko, palihug. Asa ang CR?' }
],
practicalTasks: [{ title: 'Alltagsanker', text: 'Sprich alle sechs Überlebenssätze laut durch und ordne sie drei Alltagssituationen zu.' }]
},
'Familien-Gespräche': {
learningGoals: [
'Kurze Familiengespräche sicher verstehen.',
'Nach Familienmitgliedern fragen und einfache Antworten geben.',
'Ein Mini-Gespräch über Hunger und Zuhause nachsprechen.'
],
corePatterns: [
{ target: 'Kumusta ka, Nanay?', gloss: 'Wie geht es dir, Mama?' },
{ target: 'Asa si Tatay?', gloss: 'Wo ist Papa?' },
{ target: 'Naa siya sa balay.', gloss: 'Er ist zu Hause.' },
{ target: 'Kumusta na ang Kuya?', gloss: 'Wie geht es dem älteren Bruder?' },
{ target: 'Kumusta na ang Dodong?', gloss: 'Wie geht es dem jüngeren Bruder?' },
{ target: 'Kumusta na ang Inday?', gloss: 'Wie geht es der jüngeren Schwester?' },
{ target: 'Gutom na ko, Nanay.', gloss: 'Ich habe Hunger, Mama.' },
{ target: 'Hapit na ang pagkaon.', gloss: 'Das Essen ist fast fertig.' }
],
grammarFocus: [
{ title: 'naa für Ort und Vorhandensein', text: '"Naa" hilft dir, über Orte und Vorhandensein zu sprechen.', example: 'Naa siya sa balay.' }
],
speakingPrompts: [
{ title: 'Familien-Mini-Dialog', prompt: 'Frage nach einem Familienmitglied und reagiere dann mit einer kurzen Antwort.', cue: 'Asa si Tatay? Naa siya sa balay.' }
],
practicalTasks: [{ title: 'Gesprächspraxis', text: 'Spiele einen kurzen Familienaustausch mit Frage, Antwort und Fürsorge nach.' }]
},
'Gefühle & Zuneigung': {
learningGoals: [
'Wichtige Gefühle und Zuneigungsformeln sicher unterscheiden.',
'Freundliche Nähe und Vermissen sprachlich ausdrücken.',
'Zwischen positiven und negativen Gefühlen wechseln.'
],
corePatterns: [
{ target: 'Palangga taka.', gloss: 'Ich hab dich lieb.' },
{ target: 'Ganahan ko nimo.', gloss: 'Ich mag dich.' },
{ target: 'Gimingaw ko nimo.', gloss: 'Ich vermisse dich.' },
{ target: 'Nalipay ko.', gloss: 'Ich bin glücklich.' },
{ target: 'Nasubo ko.', gloss: 'Ich bin traurig.' },
{ target: 'Nalipay ko nga nakita ka.', gloss: 'Ich freue mich, dich zu sehen.' }
],
grammarFocus: [
{ title: 'ko für eigene Gefühle', text: 'Viele Gefühlsaussagen bauen direkt auf dem Muster "Gefühl + ko" auf.', example: 'Nalipay ko. Nasubo ko.' }
],
speakingPrompts: [
{ title: 'Gefühl ausdrücken', prompt: 'Sage, dass du jemanden magst oder vermisst, und ergänze danach ein einfaches Gefühl.', cue: 'Ganahan ko nimo. Nalipay ko.' }
],
practicalTasks: [{ title: 'Herzsprache', text: 'Lies drei Zuneigungsformeln laut und entscheide danach: liebhaben, mögen oder vermissen?' }]
},
'Überlebenssätze - Teil 2': {
learningGoals: [
'Weitere zentrale Alltagsfragen sicher sprechen.',
'Höflich Entschuldigung, Nachfrage und Hilfesprache verbinden.',
'Im Alltag Preise, Dinge und Sprache klar ansprechen.'
],
corePatterns: [
{ target: 'Tagpila ni?', gloss: 'Wie viel kostet das?' },
{ target: 'Unsa ni?', gloss: 'Was ist das?' },
{ target: 'Pasensya.', gloss: 'Entschuldigung.' },
{ target: 'Dili ko mag-Bisaya.', gloss: 'Ich spreche kein Bisaya.' },
{ target: 'Palihug isulat ni.', gloss: 'Bitte schreib das auf.' },
{ target: 'Nawala ko.', gloss: 'Ich habe mich verlaufen.' }
],
grammarFocus: [
{ title: 'Kurze Markt- und Orientierungssprache', text: 'Kurze Fragewörter plus ein Nomen reichen oft, um im Alltag voranzukommen.', example: 'Tagpila ni? Unsa ni?' }
],
speakingPrompts: [
{ title: 'Auf dem Markt', prompt: 'Frage nach Preis und Bedeutung eines Gegenstands und bitte dann darum, etwas aufzuschreiben.', cue: 'Tagpila ni? Unsa ni? Palihug isulat ni.' }
],
practicalTasks: [{ title: 'Unterwegs', text: 'Sprich drei Sätze für Einkauf, Nachfrage und Orientierung laut hintereinander.' }]
}, },
'Essen & Fürsorge': { 'Essen & Fürsorge': {
learningGoals: [ learningGoals: [
@@ -53,29 +186,174 @@ const LESSON_DIDACTICS = {
'Einladungen zum Essen passend beantworten.', 'Einladungen zum Essen passend beantworten.',
'Kurze Essens-Dialoge laut üben.' 'Kurze Essens-Dialoge laut üben.'
], ],
corePatterns: ['Nikaon na ka?', 'Kaon ta.', 'Gusto ka mokaon?', 'Lami kaayo.'], corePatterns: [
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
{ target: 'Kaon ta.', gloss: 'Lass uns essen.' },
{ target: 'Gusto ka mokaon?', gloss: 'Möchtest du essen?' },
{ target: 'Gutom na ko.', gloss: 'Ich habe Hunger.' },
{ target: 'Palihug, hatagi ko ug tubig.', gloss: 'Bitte gib mir Wasser.' },
{ target: 'Salamat sa pagkaon.', gloss: 'Danke für das Essen.' },
{ target: 'Busog na ko.', gloss: 'Ich bin satt.' },
{ target: 'Lami kaayo.', gloss: 'Sehr lecker.' }
],
grammarFocus: [ grammarFocus: [
{ title: 'na als Zustandsmarker', text: '"na" markiert oft etwas, das bereits eingetreten ist oder jetzt gilt.', example: 'Nikaon na ka?' } { title: 'na als Zustandsmarker', text: '"na" markiert oft etwas, das bereits eingetreten ist oder jetzt gilt.', example: 'Nikaon na ka?' },
{ title: 'Bitten mit hatagi ko', text: 'Mit "hatagi ko" bittest du konkret darum, dass dir etwas gegeben wird.', example: 'Palihug, hatagi ko ug tubig.' }
], ],
speakingPrompts: [ speakingPrompts: [
{ title: 'Fürsorge-Dialog', prompt: 'Frage, ob jemand schon gegessen hat, und biete Essen oder Wasser an.', cue: 'Nikaon na ka? Gusto ka mokaon?' } { title: 'Fürsorge-Dialog', prompt: 'Frage, ob jemand schon gegessen hat, und biete Essen oder Wasser an.', cue: 'Nikaon na ka? Gusto ka mokaon?' },
{ title: 'Beim Essen reagieren', prompt: 'Sage, dass du Hunger hast, bitte um Wasser und reagiere danach auf das Essen.', cue: 'Gutom na ko. Palihug, hatagi ko ug tubig. Lami kaayo.' }
], ],
practicalTasks: [{ title: 'Rollenspiel', text: 'Spiele ein kurzes Gespräch zwischen Gastgeber und Gast beim Essen.' }] practicalTasks: [{ title: 'Rollenspiel', text: 'Spiele ein kurzes Gespräch zwischen Gastgeber und Gast beim Essen.' }]
}, },
'Zeitformen - Grundlagen': { 'Essen & Trinken': {
learningGoals: [ learningGoals: [
'Ni- und Mo- als einfache Zeitmarker unterscheiden.', 'Wichtige Essens- und Trinkwörter schnell erkennen.',
'Kurze Sätze in Vergangenheit und Zukunft bilden.', 'Zwischen Grundnahrungsmitteln, Getränken und Beilagen unterscheiden.',
'Das Muster laut mit mehreren Verben wiederholen.' 'Mit den neuen Wörtern kurze Einkaufs- oder Tischsätze bauen.'
],
corePatterns: [
{ target: 'Kan-on', gloss: 'gekochter Reis' },
{ target: 'Tubig', gloss: 'Wasser' },
{ target: 'Isda', gloss: 'Fisch' },
{ target: 'Manok', gloss: 'Huhn' },
{ target: 'Gulay', gloss: 'Gemüse' },
{ target: 'Prutas', gloss: 'Obst' },
{ target: 'Gatas', gloss: 'Milch' }
], ],
corePatterns: ['Ni-kaon ko.', 'Mo-kaon ko.', 'Ni-adto ko.', 'Mo-adto ko.'],
grammarFocus: [ grammarFocus: [
{ title: 'Zeitpräfixe', text: 'Ni- verweist auf Vergangenes, Mo- auf Zukünftiges oder Bevorstehendes.', example: 'Ni-kaon ko. / Mo-kaon ko.' } { title: 'Wortschatz statt ganzer Sätze', text: 'In dieser Lektion sammelst du bewusst Grundwörter, damit du später kurze Essenssätze daraus bauen kannst.', example: 'Kan-on. Tubig. Isda.' }
], ],
speakingPrompts: [ speakingPrompts: [
{ title: 'Vorher und nachher', prompt: 'Sage einen Satz über etwas, das du getan hast, und einen Satz über etwas, das du tun wirst.', cue: 'Ni-kaon ko. Mo-adto ko.' } { title: 'Auf dem Tisch', prompt: 'Nenne drei Dinge, die auf dem Tisch stehen oder die du essen und trinken möchtest.', cue: 'Kan-on, isda ug tubig.' }
], ],
practicalTasks: [{ title: 'Mustertraining', text: 'Nimm ein Verb und sprich es einmal mit Ni- und einmal mit Mo-.' }] practicalTasks: [{ title: 'Küchenrunde', text: 'Zeige nacheinander auf sieben Lebensmittel oder stelle sie dir vor und sprich jedes Wort laut aus.' }]
},
'Alltagsgespräche - Teil 1': {
learningGoals: [
'Alltagsaktivitäten in ganzen Sätzen beschreiben.',
'Nach Tagesplan, Aufgaben und Rückkehr fragen.',
'Kurze Familienabsprachen für den Tag sicher führen.'
],
corePatterns: [
{ target: 'Unsa imong buhat karon?', gloss: 'Was machst du heute?' },
{ target: 'Nagluto ko para sa panihapon.', gloss: 'Ich koche für das Abendessen.' },
{ target: 'Naglimpyo ko sa balay.', gloss: 'Ich putze das Haus.' },
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.' },
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
],
speakingPrompts: [
{ title: 'Tagesablauf abstimmen', prompt: 'Frage nach dem Plan und sage, was du heute erledigst.', cue: 'Unsa imong buhat karon? Naglimpyo ko sa balay.' }
],
practicalTasks: [{ title: 'Alltagscheck', text: 'Sprich vier Sätze zu deinem heutigen Ablauf: Aufgabe, Erledigung, Rückkehr und Pause.' }]
},
'Haus & Familie': {
learningGoals: [
'Wichtige Wörter für Haus, Räume und Familie zuordnen und aussprechen.',
'Mit „Naa … sa …“ sagen, wo sich jemand oder etwas im Haus befindet.',
'Kurze Sätze über Zuhause und Familie verstehen und nachsprechen.'
],
corePatterns: [
{ target: 'Balay', gloss: 'Haus' },
{ target: 'Kwarto', gloss: 'Zimmer' },
{ target: 'Kusina', gloss: 'Küche' },
{ target: 'Sala', gloss: 'Wohnzimmer' },
{ target: 'Banyo', gloss: 'Badezimmer' },
{ target: 'Pultahan', gloss: 'Tür' },
{ target: 'Bintana', gloss: 'Fenster' },
{ target: 'Atop', gloss: 'Dach' },
{ target: 'Pamilya', gloss: 'Familie' },
{ target: 'Among pamilya', gloss: 'unsere Familie' },
{ target: 'Naa ko sa balay.', gloss: 'Ich bin zu Hause.' },
{ target: 'Naa sila sa kusina.', gloss: 'Sie sind in der Küche.' },
{ target: 'Asa ang kusina?', gloss: 'Wo ist die Küche?' },
{ target: 'Ang among balay.', gloss: 'Unser Haus.' }
],
grammarFocus: [
{
title: 'Naa … sa … für Ort',
text: '„Naa“ drückt aus, dass jemand oder etwas irgendwo ist; „sa“ verbindet mit dem Ort.',
example: 'Naa ko sa balay. Naa sila sa kusina.'
},
{
title: 'among = unser (Plural inklusiv)',
text: '„Among“ passt zu „wir/unsere“ im Sinne von Familie oder Gruppe.',
example: 'Among pamilya. Ang among balay.'
}
],
speakingPrompts: [
{
title: 'Räume benennen',
prompt: 'Nenne Küche, Wohnzimmer und Badezimmer auf Bisaya.',
cue: 'Kusina, sala, banyo.'
},
{
title: 'Wer ist wo?',
prompt: 'Sage, dass du zu Hause bist, und frage, wo die Küche ist.',
cue: 'Naa ko sa balay. Asa ang kusina?'
}
],
practicalTasks: [
{
title: 'Rundgang',
text: 'Geh in Gedanken durch dein Zuhause und benenne jeden Raum laut auf Bisaya.'
}
]
},
'Zeitformen - Grundlagen': {
learningGoals: [
'Vergangenheit, laufende Handlung und Zukunft in einfachen Alltagssätzen unterscheiden.',
'Zeitmarker (ni-, nag-/ga-, mo-) mit Zeitwörtern sinnvoll kombinieren.',
'Zwischen denselben Verben in drei Zeitbezügen sicher wechseln.'
],
corePatterns: [
{ target: 'Ni-kaon ko ganiha.', gloss: 'Ich habe vorhin gegessen.' },
{ target: 'Nagkaon ko karon.', gloss: 'Ich esse gerade jetzt.' },
{ target: 'Mo-kaon ko unya.', gloss: 'Ich werde später essen.' },
{ target: 'Ni-adto ko sa merkado ganiha.', gloss: 'Ich bin vorhin zum Markt gegangen.' },
{ target: 'Naa ko sa merkado karon.', gloss: 'Ich bin jetzt auf dem Markt.' },
{ target: 'Mo-adto ko sa merkado ugma.', gloss: 'Ich werde morgen zum Markt gehen.' },
{ target: 'Nipalit ko og isda ganiha.', gloss: 'Ich habe vorhin Fisch gekauft.' },
{ target: 'Mupalit ko og isda ugma.', gloss: 'Ich werde morgen Fisch kaufen.' }
],
grammarFocus: [
{ title: 'Vergangenheit mit ni-', text: 'ni- markiert im Grundkurs häufig abgeschlossene Handlungen in der Vergangenheit.', example: 'Ni-kaon ko ganiha.' },
{ title: 'Laufende Handlung mit nag-/ga-', text: 'Für gerade laufende oder aktuelle Handlungen wird oft nag-/ga- genutzt, häufig zusammen mit karon.', example: 'Nagkaon ko karon.' },
{ title: 'Zukunft/Absicht mit mo-', text: 'mo- markiert im Kurs Zukünftiges oder Vorhaben und wird oft mit unya/ugma kombiniert.', example: 'Mo-adto ko ugma.' },
{ title: 'Zeitwörter als Klarsteller', text: 'Wörter wie ganiha, karon und ugma helfen, den Zeitbezug eindeutig zu machen.', example: 'ganiha (vorhin), karon (jetzt), ugma (morgen)' }
],
speakingPrompts: [
{ title: 'Dreierschritt Zeit', prompt: 'Formuliere denselben Inhalt nacheinander für Vergangenheit, Gegenwart und Zukunft.', cue: 'Ni-adto ko ganiha. Naa ko diri karon. Mo-adto ko ugma.' },
{ title: 'Tagesplanung mit Zeiten', prompt: 'Sage, was du vorhin getan hast, was du jetzt machst und was du später tun wirst.', cue: 'Nipalit ko ganiha. Nagluto ko karon. Mo-kaon ko unya.' }
],
practicalTasks: [
{ title: 'Verb-Staffel', text: 'Nimm drei Verben (z. B. kaon, adto, palit) und bilde jeweils Vergangenheit, Gegenwart und Zukunft laut.' },
{ title: 'Zeitkarten', text: 'Ziehe zufällig ein Zeitwort (ganiha/karon/unya/ugma) und bilde sofort einen passenden Satz.' }
]
},
'Alltagsgespräche - Teil 2': {
learningGoals: [
'Ziele, Wege und Zeitpunkte im Alltag genauer angeben.',
'Verabredungen und Rückkehrzeiten in ganzen Sätzen formulieren.',
'Einkaufs- und Familienwege natürlich besprechen.'
],
corePatterns: [
{ target: 'Asa ka moadto unya?', gloss: 'Wohin gehst du später?' },
{ target: 'Moadto ko sa merkado unya.', gloss: 'Ich gehe später zum Markt.' },
{ target: 'Unsa imong plano karong gabii?', gloss: 'Was ist dein Plan heute Abend?' },
{ target: 'Magkita mi sa silingan karon.', gloss: 'Wir treffen jetzt die Nachbarn.' },
{ target: 'Mupalit ko ug pagkaon para sa balay.', gloss: 'Ich kaufe Essen für zu Hause.' },
{ target: 'Mahimo ba ta magkita ugma buntag?', gloss: 'Können wir uns morgen früh treffen?' },
{ target: 'Unsa orasa ka mouli?', gloss: 'Um wie viel Uhr kommst du nach Hause?' },
{ target: 'Mouli ko mga alas sais.', gloss: 'Ich komme gegen sechs nach Hause.' }
],
speakingPrompts: [
{ title: 'Weg und Zeit planen', prompt: 'Frage nach Ziel und Uhrzeit und gib eine konkrete Antwort.', cue: 'Asa ka moadto unya? Mouli ko mga alas sais.' }
],
practicalTasks: [{ title: 'Planungsdialog', text: 'Baue einen Mini-Dialog mit Zielort, Uhrzeit und Rückkehr in mindestens vier Sätzen.' }]
}, },
'Woche 1 - Wiederholung': { 'Woche 1 - Wiederholung': {
learningGoals: [ learningGoals: [
@@ -95,9 +373,19 @@ const LESSON_DIDACTICS = {
'Von einzelnen Wörtern zu kurzen Sätzen übergehen.' 'Von einzelnen Wörtern zu kurzen Sätzen übergehen.'
], ],
corePatterns: ['Kumusta', 'Salamat', 'Lami', 'Mingaw ko nimo'] corePatterns: ['Kumusta', 'Salamat', 'Lami', 'Mingaw ko nimo']
} },
...BISAYA_RELATIONSHIP_ANCHOR_DIDACTICS,
...BISAYA_DIDACTICS_24_43
}; };
function resolveDidacticsForLesson(lesson) {
const direct = LESSON_DIDACTICS[lesson.title];
if (direct) return direct;
const plannedTitle = BISAYA_LESSONS_24_43_BY_NUMBER[Number(lesson.lessonNumber)]?.title;
if (plannedTitle && LESSON_DIDACTICS[plannedTitle]) return LESSON_DIDACTICS[plannedTitle];
return null;
}
async function resetBisayaProgress(courseIds) { async function resetBisayaProgress(courseIds) {
if (courseIds.length === 0) return { lessonProgress: 0, exerciseProgress: 0 }; if (courseIds.length === 0) return { lessonProgress: 0, exerciseProgress: 0 };
@@ -170,15 +458,36 @@ async function applyBisayaCourseRefresh() {
}); });
for (const lesson of lessons) { for (const lesson of lessons) {
const didactics = LESSON_DIDACTICS[lesson.title]; const didactics = resolveDidacticsForLesson(lesson);
if (!didactics) continue; const plannedLesson = BISAYA_LESSONS_24_43_BY_NUMBER[Number(lesson.lessonNumber)];
const pedagogy = getBisayaLessonPedagogy(lesson.lessonNumber);
if (!didactics && !pedagogy) continue;
const normalizedDidactics = didactics || {};
await lesson.update({ await lesson.update({
learningGoals: didactics.learningGoals || [], ...(plannedLesson ? {
corePatterns: didactics.corePatterns || [], title: plannedLesson.title,
grammarFocus: didactics.grammarFocus || [], description: plannedLesson.desc,
speakingPrompts: didactics.speakingPrompts || [], weekNumber: plannedLesson.week,
practicalTasks: didactics.practicalTasks || [] dayNumber: plannedLesson.day,
lessonType: plannedLesson.type,
culturalNotes: plannedLesson.cultural,
targetMinutes: plannedLesson.targetMin,
targetScorePercent: plannedLesson.targetScore,
requiresReview: plannedLesson.review
} : {}),
learningGoals: normalizedDidactics.learningGoals || [],
corePatterns: normalizedDidactics.corePatterns || [],
grammarFocus: normalizedDidactics.grammarFocus || [],
speakingPrompts: normalizedDidactics.speakingPrompts || [],
practicalTasks: normalizedDidactics.practicalTasks || [],
didacticMode: pedagogy?.didacticMode || null,
phaseLabel: pedagogy?.phaseLabel || null,
blockNumber: pedagogy?.blockNumber ?? null,
difficultyWeight: pedagogy?.difficultyWeight ?? null,
newUnitTarget: pedagogy?.newUnitTarget ?? null,
reviewWeight: pedagogy?.reviewWeight ?? null,
isIntensiveReview: Boolean(pedagogy?.isIntensiveReview)
}); });
updatedLessons++; updatedLessons++;
} }

View File

@@ -0,0 +1,188 @@
export const BISAYA_PHASE1_DIDACTICS = {
'Begrüßungen & Höflichkeit': {
corePatterns: [
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
{ target: 'Maayong buntag.', gloss: 'Guten Morgen.' },
{ target: 'Maayong adlaw.', gloss: 'Guten Tag.' },
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
{ target: 'Maayong gabii, matulog na ta.', gloss: 'Guten Abend, wir legen uns schlafen.' },
{ target: 'Katulog og maayo.', gloss: 'Schlaf gut.' },
{ target: 'Kapoy na ka?', gloss: 'Bist du müde?' },
{ target: 'Matulog na ta.', gloss: 'Lass uns schlafen gehen.' },
{ target: 'Inom sa og tubig.', gloss: 'Trink Wasser.' },
{ target: 'Patya ang suga.', gloss: 'Mach das Licht aus.' },
{ target: 'Tabuni ang imong kaugalingon.', gloss: 'Deck dich zu.' },
{ target: 'Ugma nasad.', gloss: 'Bis morgen wieder.' },
{ target: 'Damgo og nindot.', gloss: 'Träum schön.' },
{ target: 'Amping.', gloss: 'Pass auf dich auf.' },
{ target: 'Babay.', gloss: 'Tschüss.' },
{ target: 'Maayo ko.', gloss: 'Mir geht es gut.' },
{ target: 'Salamat.', gloss: 'Danke.' },
{ target: 'Palihug.', gloss: 'Bitte.' }
]
}
};
/**
* Volle Didaktik-Snippets für Lektionen, die in update-bisaya-didactics.js gepflegt werden sollen
* und als API-Fallback dienen, wenn core_patterns in der DB fehlen (z. B. Kurz-Wiederholung).
*/
export const BISAYA_DIDACTICS_FRAGMENTS = {
'Alltagsgespräche - Teil 1': {
learningGoals: [
'Alltagsaktivitäten in ganzen Sätzen beschreiben.',
'Nach Tagesplan, Aufgaben und Rückkehr fragen.',
'Kurze Familienabsprachen für den Tag sicher führen.'
],
corePatterns: [
{ target: 'Unsa imong buhat karon?', gloss: 'Was machst du heute?' },
{ target: 'Nagluto ko para sa panihapon.', gloss: 'Ich koche für das Abendessen.' },
{ target: 'Naglimpyo ko sa balay.', gloss: 'Ich putze das Haus.' },
{ target: 'Human na ka sa trabaho?', gloss: 'Bist du mit der Arbeit fertig?' },
{ target: 'Dali lang ko mubalik.', gloss: 'Ich komme gleich wieder.' },
{ target: 'Naa koy lakaw karong hapon.', gloss: 'Ich habe heute Nachmittag etwas zu erledigen.' },
{ target: 'Magpahuway ko gamay unya.', gloss: 'Ich ruhe mich später kurz aus.' },
{ target: 'Tawagi ko kung mahuman ka.', gloss: 'Ruf mich an, wenn du fertig bist.' }
],
speakingPrompts: [
{
title: 'Tagesablauf abstimmen',
prompt: 'Frage nach dem Plan und sage, was du heute erledigst.',
cue: 'Unsa imong buhat karon? Naglimpyo ko sa balay.'
}
],
practicalTasks: [
{
title: 'Alltagscheck',
text: 'Sprich vier Sätze zu deinem heutigen Ablauf: Aufgabe, Erledigung, Rückkehr und Pause.'
}
]
},
'Haus & Familie': {
learningGoals: [
'Wichtige Wörter für Haus, Räume und Familie zuordnen und aussprechen.',
'Mit „Naa … sa …“ sagen, wo sich jemand oder etwas im Haus befindet.',
'Kurze Sätze über Zuhause und Familie verstehen und nachsprechen.'
],
corePatterns: [
{ target: 'Balay', gloss: 'Haus' },
{ target: 'Kwarto', gloss: 'Zimmer' },
{ target: 'Kusina', gloss: 'Küche' },
{ target: 'Sala', gloss: 'Wohnzimmer' },
{ target: 'Banyo', gloss: 'Badezimmer' },
{ target: 'Pultahan', gloss: 'Tür' },
{ target: 'Bintana', gloss: 'Fenster' },
{ target: 'Atop', gloss: 'Dach' },
{ target: 'Pamilya', gloss: 'Familie' },
{ target: 'Among pamilya', gloss: 'unsere Familie' },
{ target: 'Naa ko sa balay.', gloss: 'Ich bin zu Hause.' },
{ target: 'Naa sila sa kusina.', gloss: 'Sie sind in der Küche.' },
{ target: 'Asa ang kusina?', gloss: 'Wo ist die Küche?' },
{ target: 'Ang among balay.', gloss: 'Unser Haus.' }
],
grammarFocus: [
{
title: 'Naa … sa … für Ort',
text: '„Naa“ drückt aus, dass jemand oder etwas irgendwo ist; „sa“ verbindet mit dem Ort.',
example: 'Naa ko sa balay. Naa sila sa kusina.'
},
{
title: 'among = unser (Plural inklusiv)',
text: '„Among“ passt zu „wir/unsere“ im Sinne von Familie oder Gruppe.',
example: 'Among pamilya. Ang among balay.'
}
],
speakingPrompts: [
{
title: 'Räume benennen',
prompt: 'Nenne Küche, Wohnzimmer und Badezimmer auf Bisaya.',
cue: 'Kusina, sala, banyo.'
},
{
title: 'Wer ist wo?',
prompt: 'Sage, dass du zu Hause bist, und frage, wo die Küche ist.',
cue: 'Naa ko sa balay. Asa ang kusina?'
}
],
practicalTasks: [
{
title: 'Rundgang',
text: 'Geh in Gedanken durch dein Zuhause und benenne jeden Raum laut auf Bisaya.'
}
]
},
'Alltagsgespräche - Teil 2': {
learningGoals: [
'Ziele, Wege und Zeitpunkte im Alltag genauer angeben.',
'Verabredungen und Rückkehrzeiten in ganzen Sätzen formulieren.',
'Einkaufs- und Familienwege natürlich besprechen.'
],
corePatterns: [
{ target: 'Asa ka moadto unya?', gloss: 'Wohin gehst du später?' },
{ target: 'Moadto ko sa merkado unya.', gloss: 'Ich gehe später zum Markt.' },
{ target: 'Unsa imong plano karong gabii?', gloss: 'Was ist dein Plan heute Abend?' },
{ target: 'Magkita mi sa silingan karon.', gloss: 'Wir treffen jetzt die Nachbarn.' },
{ target: 'Mupalit ko ug pagkaon para sa balay.', gloss: 'Ich kaufe Essen für zu Hause.' },
{ target: 'Mahimo ba ta magkita ugma buntag?', gloss: 'Können wir uns morgen früh treffen?' },
{ target: 'Unsa orasa ka mouli?', gloss: 'Um wie viel Uhr kommst du nach Hause?' },
{ target: 'Mouli ko mga alas sais.', gloss: 'Ich komme gegen sechs nach Hause.' }
],
speakingPrompts: [
{
title: 'Weg und Zeit planen',
prompt: 'Frage nach Ziel und Uhrzeit und gib eine konkrete Antwort.',
cue: 'Asa ka moadto unya? Mouli ko mga alas sais.'
}
],
practicalTasks: [
{
title: 'Planungsdialog',
text: 'Baue einen Mini-Dialog mit Zielort, Uhrzeit und Rückkehr in mindestens vier Sätzen.'
}
]
},
'Ort & Richtung': {
learningGoals: [
'Fragen nach Ort und Richtung sicher stellen und beantworten.',
'„hier“, „dort“ und „unterwegs nach …“ im Alltag unterscheiden.',
'Kurze Weg- und Zielangaben mit bekannten Ortswörtern verbinden.'
],
corePatterns: [
{ target: 'Asa ka?', gloss: 'Wo bist du? / Wohin gehst du?' },
{ target: 'Asa ang merkado?', gloss: 'Wo ist der Markt?' },
{ target: 'Asa ang simbahan?', gloss: 'Wo ist die Kirche?' },
{ target: 'Dinhi ko.', gloss: 'Ich bin hier.' },
{ target: 'Naa ko dinhi.', gloss: 'Ich bin hier.' },
{ target: 'Didto siya.', gloss: 'Er/sie ist dort.' },
{ target: 'Adto ta didto.', gloss: 'Lass uns dorthin gehen.' },
{ target: 'Padulong ko sa merkado.', gloss: 'Ich bin auf dem Weg zum Markt.' },
{ target: 'Padulong ta didto.', gloss: 'Lass uns dorthin aufbrechen.' },
{ target: 'Naa ko sa balay.', gloss: 'Ich bin zu Hause.' }
],
grammarFocus: [
{
title: 'Asa = Wo / Wohin',
text: '„Asa“ fragt nach Ort oder Ziel. Mit „ang“ kannst du nach einem bestimmten Ort fragen.',
example: 'Asa ka? Asa ang merkado?'
},
{
title: 'dinhi / didto',
text: '„dinhi“ = hier, „didto“ = dort (weg vom Sprecher).',
example: 'Naa ko dinhi. Didto ang simbahan.'
}
],
speakingPrompts: [
{
title: 'Weg fragen',
prompt: 'Frage, wo der Markt ist, und sage dann, dass du dorthin unterwegs bist.',
cue: 'Asa ang merkado? Padulong ko sa merkado.'
}
],
practicalTasks: [
{
title: 'Mini-Route',
text: 'Bilde drei Sätze: wo du bist, wohin du willst, und dass du unterwegs bist.'
}
]
}
};

View File

@@ -0,0 +1,160 @@
export const BISAYA_LESSON_PEDAGOGY = {
1: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 2, newUnitTarget: 5, reviewWeight: 15, isIntensiveReview: false },
2: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 8, reviewWeight: 20, isIntensiveReview: false },
3: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 2, newUnitTarget: 7, reviewWeight: 20, isIntensiveReview: false },
4: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 25, isIntensiveReview: false },
5: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 4, reviewWeight: 25, isIntensiveReview: false },
6: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 7, reviewWeight: 25, isIntensiveReview: false },
7: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 30, isIntensiveReview: false },
8: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'core_input', difficultyWeight: 2, newUnitTarget: 6, reviewWeight: 30, isIntensiveReview: false },
9: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 90, isIntensiveReview: true },
10: { phaseLabel: 'quickstart', blockNumber: 1, didacticMode: 'checkpoint', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: false },
11: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 25, isIntensiveReview: false },
12: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 2, newUnitTarget: 7, reviewWeight: 25, isIntensiveReview: false },
13: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 30, isIntensiveReview: false },
14: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 30, isIntensiveReview: false },
15: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 40, isIntensiveReview: false },
16: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 35, isIntensiveReview: false },
17: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 35, isIntensiveReview: false },
18: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 7, reviewWeight: 35, isIntensiveReview: false },
19: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 8, reviewWeight: 36, isIntensiveReview: false },
20: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 7, reviewWeight: 36, isIntensiveReview: false },
21: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 6, reviewWeight: 36, isIntensiveReview: false },
22: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
23: { phaseLabel: 'quickstart', blockNumber: 2, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: false },
24: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 35, isIntensiveReview: false },
25: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 7, reviewWeight: 35, isIntensiveReview: false },
26: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 40, isIntensiveReview: false },
27: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'core_input', difficultyWeight: 4, newUnitTarget: 6, reviewWeight: 40, isIntensiveReview: false },
28: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 45, isIntensiveReview: false },
29: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
30: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 40, isIntensiveReview: false },
31: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 40, isIntensiveReview: false },
32: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
33: { phaseLabel: 'daily_life', blockNumber: 3, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
34: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
35: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
36: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 60, isIntensiveReview: false },
37: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
38: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 65, isIntensiveReview: false },
39: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
40: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 70, isIntensiveReview: false },
41: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
42: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
43: { phaseLabel: 'stabilization', blockNumber: 4, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 2, reviewWeight: 50, isIntensiveReview: false },
44: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 40, isIntensiveReview: false },
45: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 40, isIntensiveReview: false },
46: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 45, isIntensiveReview: false },
47: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
48: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
49: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 90, isIntensiveReview: true },
50: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
51: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 45, isIntensiveReview: false },
52: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
53: { phaseLabel: 'daily_life', blockNumber: 5, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
54: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
55: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
56: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
57: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
58: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 65, isIntensiveReview: false },
59: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
60: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 70, isIntensiveReview: false },
61: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
62: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
63: { phaseLabel: 'stabilization', blockNumber: 6, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 2, reviewWeight: 60, isIntensiveReview: false },
64: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 45, isIntensiveReview: false },
65: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 45, isIntensiveReview: false },
66: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 50, isIntensiveReview: false },
67: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
68: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
69: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
70: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
71: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
72: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
73: { phaseLabel: 'daily_life', blockNumber: 7, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
74: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
75: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
76: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
77: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
78: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
79: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
80: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
81: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
82: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
83: { phaseLabel: 'daily_life', blockNumber: 8, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
84: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
85: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
86: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
87: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
88: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
89: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
90: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
91: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
92: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
93: { phaseLabel: 'daily_life', blockNumber: 9, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
94: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 50, isIntensiveReview: false },
95: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 50, isIntensiveReview: false },
96: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 55, isIntensiveReview: false },
97: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'guided_dialogue', difficultyWeight: 3, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
98: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
99: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'intensive_review', difficultyWeight: 3, newUnitTarget: 0, reviewWeight: 92, isIntensiveReview: true },
100: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 55, isIntensiveReview: false },
101: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 55, isIntensiveReview: false },
102: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
103: { phaseLabel: 'daily_life', blockNumber: 10, didacticMode: 'checkpoint', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: false },
104: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 60, isIntensiveReview: false },
105: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 60, isIntensiveReview: false },
106: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'pattern_drill', difficultyWeight: 4, newUnitTarget: 4, reviewWeight: 65, isIntensiveReview: false },
107: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 65, isIntensiveReview: false },
108: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 97, isIntensiveReview: true },
109: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
110: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'guided_dialogue', difficultyWeight: 4, newUnitTarget: 5, reviewWeight: 65, isIntensiveReview: false },
111: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 6, reviewWeight: 65, isIntensiveReview: false },
112: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
113: { phaseLabel: 'stabilization', blockNumber: 11, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
114: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 70, isIntensiveReview: false },
115: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
116: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 72, isIntensiveReview: false },
117: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 80, isIntensiveReview: false },
118: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
119: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 96, isIntensiveReview: true },
120: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 75, isIntensiveReview: false },
121: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
122: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
123: { phaseLabel: 'stabilization', blockNumber: 12, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 2, reviewWeight: 65, isIntensiveReview: false },
124: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 72, isIntensiveReview: false },
125: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
126: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 82, isIntensiveReview: false },
127: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 74, isIntensiveReview: false },
128: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
129: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
130: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 75, isIntensiveReview: false },
131: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
132: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
133: { phaseLabel: 'stabilization', blockNumber: 13, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
134: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 74, isIntensiveReview: false },
135: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 94, isIntensiveReview: true },
136: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 84, isIntensiveReview: false },
137: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 76, isIntensiveReview: false },
138: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 98, isIntensiveReview: true },
139: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
140: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 3, reviewWeight: 78, isIntensiveReview: false },
141: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 4, newUnitTarget: 0, reviewWeight: 95, isIntensiveReview: true },
142: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
143: { phaseLabel: 'stabilization', blockNumber: 14, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: false },
144: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
145: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 99, isIntensiveReview: true },
146: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'pattern_drill', difficultyWeight: 5, newUnitTarget: 1, reviewWeight: 88, isIntensiveReview: false },
147: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 80, isIntensiveReview: false },
148: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: true },
149: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'intensive_review', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: true },
150: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'real_life_scenario', difficultyWeight: 5, newUnitTarget: 2, reviewWeight: 82, isIntensiveReview: false },
151: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
152: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'checkpoint', difficultyWeight: 5, newUnitTarget: 0, reviewWeight: 100, isIntensiveReview: false },
153: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'real_life_scenario', difficultyWeight: 2, newUnitTarget: 1, reviewWeight: 70, isIntensiveReview: false },
154: { phaseLabel: 'stabilization', blockNumber: 15, didacticMode: 'core_input', difficultyWeight: 3, newUnitTarget: 10, reviewWeight: 76, isIntensiveReview: false }
};
export function getBisayaLessonPedagogy(lessonNumber) {
return BISAYA_LESSON_PEDAGOGY[Number(lessonNumber)] || null;
}

View File

@@ -0,0 +1,558 @@
export const BISAYA_PHASE3_DIDACTICS = {
'Besuch & Gastfreundschaft': {
learningGoals: [
'Besuchssituationen sicher eröffnen und freundlich strukturieren.',
'Hereinbitten, Sitzplatz anbieten, Dank ausdrücken und einfache Rückfragen stellen.',
'Bekannte Familien- und Fürsorgemuster in Besuchsgespräche übertragen.',
'Eine kurze Besuchsszene mit mindestens vier eigenen Sätzen sprechen.'
],
corePatterns: [
{ target: 'Sulod lang.', gloss: 'Komm ruhig herein.' },
{ target: 'Lingkod sa.', gloss: 'Setz dich erst einmal.' },
{ target: 'Maayong pag-abot.', gloss: 'Willkommen.' },
{ target: 'Salamat sa pag-anhi.', gloss: 'Danke fürs Kommen.' },
{ target: 'Gimingaw mi nimo.', gloss: 'Wir haben dich vermisst.' },
{ target: 'Kumusta ang biyahe?', gloss: 'Wie war die Reise?' },
{ target: 'Ganahan ka og tubig?', gloss: 'Möchtest du Wasser?' },
{ target: 'Naa ra mi diri.', gloss: 'Wir sind hier.' }
],
grammarFocus: [
{ title: 'lang und sa als weiche Partikel', text: 'lang und sa machen Aufforderungen oft weniger hart und alltagstauglicher.', example: 'Sulod lang. Lingkod sa.' },
{ title: 'mi statt ko', text: 'mi bedeutet wir und passt, wenn du für deine Familie oder Gruppe sprichst.', example: 'Gimingaw mi nimo.' }
],
speakingPrompts: [
{ title: 'Gast empfangen', prompt: 'Empfange einen Gast, bitte ihn herein, biete einen Sitzplatz an und frage nach der Reise.', cue: 'Maayong pag-abot. Sulod lang. Lingkod sa. Kumusta ang biyahe?' },
{ title: 'Familiäre Wärme', prompt: 'Sage, dass ihr die Person vermisst habt und biete Wasser an.', cue: 'Gimingaw mi nimo. Ganahan ka og tubig?' }
],
practicalTasks: [
{ title: 'Besuchsszene', text: 'Spiele eine Besuchsszene mit Begrüßung, Einladung, Angebot und Rückfrage.' }
]
},
'Besuch & Haushalt': {
learningGoals: [
'Wichtige Haus- und Besuchswörter sicher erkennen.',
'Dinge im Haus zeigen, benennen und in kurzen Sätzen verwenden.',
'Besuchswortschatz mit bereits bekannten Fragen verbinden.',
'Mindestens sechs Haushaltswörter aktiv abrufen.'
],
corePatterns: [
{ target: 'bisita', gloss: 'Besuch / Gast' },
{ target: 'balay', gloss: 'Haus / Zuhause' },
{ target: 'kwarto', gloss: 'Zimmer' },
{ target: 'kusina', gloss: 'Küche' },
{ target: 'lamisa', gloss: 'Tisch' },
{ target: 'lingkuranan', gloss: 'Stuhl' },
{ target: 'pultahan', gloss: 'Tür' },
{ target: 'Naa sa kusina.', gloss: 'Es ist in der Küche.' },
{ target: 'Asa ang lamisa?', gloss: 'Wo ist der Tisch?' }
],
grammarFocus: [
{ title: 'Naa sa ...', text: 'Mit naa sa sagst du, wo etwas ist.', example: 'Naa sa kusina. Naa sa kwarto.' }
],
speakingPrompts: [
{ title: 'Im Haus zeigen', prompt: 'Sage, wo Tisch, Stuhl und Küche sind.', cue: 'Naa sa kusina. Naa ang lingkuranan diri.' }
],
practicalTasks: [
{ title: 'Hausrunde', text: 'Gehe gedanklich durch ein Haus und nenne sechs Dinge mit Ort.' }
]
},
'Fragen im Alltag vertiefen': {
learningGoals: [
'Mehrstufige Frageketten bilden, ohne in lange deutsche Satzstrukturen zu fallen.',
'Ziel, Vorhaben, Zeit und Bedeutung höflich erfragen.',
'Bei Unsicherheit nachhaken und eine Frage reparieren.',
'Drei kurze Folgefragen flüssig hintereinander sprechen.'
],
corePatterns: [
{ target: 'Asa ka padulong?', gloss: 'Wohin gehst du?' },
{ target: 'Unsa imong buhaton?', gloss: 'Was wirst du machen?' },
{ target: 'Kanus-a ka moadto?', gloss: 'Wann gehst du hin?' },
{ target: 'Kinsa imong kuyog?', gloss: 'Mit wem bist du unterwegs?' },
{ target: 'Pwede ko mangutana?', gloss: 'Darf ich fragen?' },
{ target: 'Unsay pasabot ani?', gloss: 'Was bedeutet das?' },
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' }
],
grammarFocus: [
{ title: 'Frageketten', text: 'Bisaya-Alltagsgespräche nutzen oft mehrere kurze Fragen statt einer langen Konstruktion.', example: 'Asa ka padulong? Unsa imong buhaton didto?' },
{ title: 'ka und imong', text: 'ka bezieht sich auf du als Person, imong auf dein/deine.', example: 'Asa ka padulong? Unsa imong buhaton?' }
],
speakingPrompts: [
{ title: 'Drei-Fragen-Kette', prompt: 'Frage nach Ziel, Zeit und Begleitung.', cue: 'Asa ka padulong? Kanus-a ka moadto? Kinsa imong kuyog?' },
{ title: 'Reparaturfrage', prompt: 'Sage, dass du fragen möchtest, bitte um Wiederholung und frage nach Bedeutung.', cue: 'Pwede ko mangutana? Palihug ka mubalik? Unsay pasabot ani?' }
],
practicalTasks: [
{ title: 'Frageleiter', text: 'Baue aus drei kurzen Fragen eine natürliche Alltagsszene.' }
]
},
'Termine & Verabredungen': {
learningGoals: [
'Einfache Treffen verabreden und bestätigen.',
'Zeitangaben mit morgen, später, heute und Uhrzeit verbinden.',
'Zusage, Absage und Verschieben weich ausdrücken.',
'Einen kurzen Termin-Dialog mit Uhrzeit sprechen.'
],
corePatterns: [
{ target: 'Magkita ta ugma.', gloss: 'Wir treffen uns morgen.' },
{ target: 'Unsa orasa?', gloss: 'Um wie viel Uhr?' },
{ target: 'Pwede ko karon.', gloss: 'Ich kann jetzt / heute.' },
{ target: 'Dili ko pwede karon.', gloss: 'Ich kann jetzt / heute nicht.' },
{ target: 'Pwede ugma?', gloss: 'Geht morgen?' },
{ target: 'Sige, kita ta.', gloss: 'Okay, wir sehen uns.' },
{ target: 'Human sa trabaho.', gloss: 'Nach der Arbeit.' },
{ target: 'Texti lang ko.', gloss: 'Schreib mir einfach.' }
],
grammarFocus: [
{ title: 'ta für wir beide / lass uns', text: 'ta verbindet dich und die andere Person in gemeinsamen Handlungen.', example: 'Magkita ta ugma. Kita ta.' },
{ title: 'pwede für Möglichkeit', text: 'pwede hilft bei Zusage, Frage und Absage.', example: 'Pwede ko karon. Pwede ugma?' }
],
speakingPrompts: [
{ title: 'Treffen planen', prompt: 'Vereinbare ein Treffen für morgen, frage nach der Uhrzeit und bestätige.', cue: 'Magkita ta ugma. Unsa orasa? Sige, kita ta.' },
{ title: 'Termin verschieben', prompt: 'Sage, dass du heute nicht kannst, und schlage morgen vor.', cue: 'Dili ko pwede karon. Pwede ugma?' }
],
practicalTasks: [
{ title: 'Kalenderdialog', text: 'Sprich zwei Varianten: eine Zusage und eine weiche Verschiebung.' }
]
},
'Woche 5 - Intensivwiederholung I': {
learningGoals: [
'Besuch, Familie, Fürsorge und Terminplanung unter Abrufdruck mischen.',
'Neue Muster mit alten Grundlagen aus Begrüßung, Essen, Zeit und Beziehung verbinden.',
'Schnell zwischen Rollen wechseln: Gastgeber, Gast, Familienmitglied.',
'Mindestens acht Situationsantworten ohne deutsche Stütze geben.'
],
corePatterns: [
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
{ target: 'Sulod lang.', gloss: 'Komm ruhig herein.' },
{ target: 'Lingkod sa.', gloss: 'Setz dich erst einmal.' },
{ target: 'Magkita ta ugma.', gloss: 'Wir treffen uns morgen.' },
{ target: 'Unsa orasa?', gloss: 'Um wie viel Uhr?' },
{ target: 'Ganahan ka og tubig?', gloss: 'Möchtest du Wasser?' },
{ target: 'Palangga taka.', gloss: 'Ich hab dich lieb / Ich liebe dich.' }
],
speakingPrompts: [
{ title: 'Rollenwechsel', prompt: 'Wechsle zwischen Gast empfangen, nach Essen fragen und Termin vereinbaren.', cue: 'Sulod lang. Nikaon na ka? Magkita ta ugma.' }
],
practicalTasks: [
{ title: 'Acht schnelle Antworten', text: 'Beantworte acht kurze Situationen aus Besuch, Familie und Terminplanung laut.' }
]
},
'Spiralwiederholung - Familie & Fürsorge': {
learningGoals: [
'Frühe Familien- und Fürsorgemuster aktiv festigen.',
'Familienwörter nicht isoliert, sondern in kurzen Handlungen verwenden.',
'Nähe, Essen, Müdigkeit und Hilfe in einem Mini-Dialog verbinden.',
'Alte Kernmuster mit neuen Besuchssätzen kombinieren.'
],
corePatterns: [
{ target: 'Nanay', gloss: 'Mutter' },
{ target: 'Tatay', gloss: 'Vater' },
{ target: 'anak', gloss: 'Kind' },
{ target: 'pamilya', gloss: 'Familie' },
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
{ target: 'Kapoy na ka?', gloss: 'Bist du schon müde?' },
{ target: 'Tabangi ko, palihug.', gloss: 'Hilf mir bitte.' },
{ target: 'Palangga taka.', gloss: 'Ich hab dich lieb / Ich liebe dich.' }
],
speakingPrompts: [
{ title: 'Familien-Mini-Dialog', prompt: 'Sprich mit einem Familienmitglied über Essen, Müdigkeit und Hilfe.', cue: 'Nikaon na ka? Kapoy na ka? Tabangi ko, palihug.' }
],
practicalTasks: [
{ title: 'Spiralabruf', text: 'Nimm vier alte Familienwörter und bilde mit jedem einen kurzen Alltagssatz.' }
]
},
'Gesundheit im Alltag': {
learningGoals: [
'Einfache Beschwerden erfragen, beantworten und fürsorglich reagieren.',
'Körper, Medizin, Ruhe und Wasser in einem Pflegegespräch verbinden.',
'Zwischen Frage, Rat und Rückmeldung unterscheiden.',
'Ein Gesundheitsgespräch mit mindestens fünf Sätzen sprechen.'
],
corePatterns: [
{ target: 'Sakit imong ulo?', gloss: 'Tut dein Kopf weh?' },
{ target: 'Sakit akong tiyan.', gloss: 'Mein Bauch tut weh.' },
{ target: 'Niinom ka og tambal?', gloss: 'Hast du Medizin genommen?' },
{ target: 'Magpahuway sa.', gloss: 'Ruh dich erst einmal aus.' },
{ target: 'Uminom og tubig.', gloss: 'Trink Wasser.' },
{ target: 'Mas maayo na ka?', gloss: 'Geht es dir schon besser?' },
{ target: 'Maayo ra ko.', gloss: 'Mir geht es okay.' },
{ target: 'Tawag ta og doktor?', gloss: 'Sollen wir einen Arzt rufen?' }
],
grammarFocus: [
{ title: 'akong und imong', text: 'akong bedeutet mein, imong bedeutet dein. Beides steht direkt vor dem Körperteil.', example: 'akong tiyan, imong ulo' },
{ title: 'Frage mit na', text: 'na kann schon/jetzt markieren und kommt in Gesundheitsfragen häufig vor.', example: 'Mas maayo na ka?' }
],
speakingPrompts: [
{ title: 'Pflegegespräch', prompt: 'Frage nach Schmerzen, biete Ruhe und Wasser an und frage nach Besserung.', cue: 'Sakit imong ulo? Magpahuway sa. Uminom og tubig. Mas maayo na ka?' }
],
practicalTasks: [
{ title: 'Fünf-Satz-Szene', text: 'Sprich eine Szene: Beschwerde, Nachfrage, Medizin, Ruhe, Besserung.' }
]
},
'Medikamente & Beschwerden': {
learningGoals: [
'Gesundheitswortschatz zu Symptomen, Körper und Hilfe sicher abrufen.',
'Symptomwörter in kurze Fürsorgesätze einbauen.',
'Zwischen Wortabruf und situativer Reaktion wechseln.',
'Mindestens acht Gesundheitsbegriffe aktiv verwenden.'
],
corePatterns: [
{ target: 'tambal', gloss: 'Medizin' },
{ target: 'ubo', gloss: 'Husten' },
{ target: 'hilanat', gloss: 'Fieber' },
{ target: 'kasakit', gloss: 'Schmerz' },
{ target: 'ulo', gloss: 'Kopf' },
{ target: 'tiyan', gloss: 'Bauch' },
{ target: 'doktor', gloss: 'Arzt / Ärztin' },
{ target: 'tubig', gloss: 'Wasser' },
{ target: 'Aduna kay hilanat?', gloss: 'Hast du Fieber?' }
],
speakingPrompts: [
{ title: 'Symptom zu Hilfe', prompt: 'Ordne Husten, Fieber und Schmerz je einer passenden Hilfe zu.', cue: 'Aduna kay hilanat? Magpahuway sa. Uminom og tubig.' }
],
practicalTasks: [
{ title: 'Gesundheitskarten', text: 'Ziehe gedanklich acht Gesundheitswörter und bilde daraus vier kurze Hilfesätze.' }
]
},
'Woche 5 - Intensivwiederholung II': {
learningGoals: [
'Gesundheit, Besuch, Fragen und Terminplanung gemischt reaktivieren.',
'Ähnliche Muster gezielt unterscheiden: Frage, Bitte, Angebot, Rat.',
'Unter leichtem Zeitdruck verständlich reagieren.',
'Fehlercluster aus Woche 5 für SRS und Wiederholung sichtbar machen.'
],
corePatterns: [
{ target: 'Pwede ko mangutana?', gloss: 'Darf ich fragen?' },
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
{ target: 'Magkita ta ugma.', gloss: 'Wir treffen uns morgen.' },
{ target: 'Salamat sa pag-anhi.', gloss: 'Danke fürs Kommen.' },
{ target: 'Niinom ka og tambal?', gloss: 'Hast du Medizin genommen?' },
{ target: 'Mas maayo na ka?', gloss: 'Geht es dir schon besser?' },
{ target: 'Dili ko pwede karon.', gloss: 'Ich kann heute nicht.' },
{ target: 'Ganahan ka og tubig?', gloss: 'Möchtest du Wasser?' }
],
speakingPrompts: [
{ title: 'Mischrunde', prompt: 'Reagiere nacheinander auf Besuch, Krankheit, Termin und Verständnisproblem.', cue: 'Sulod lang. Niinom ka og tambal? Pwede ugma? Palihug ka mubalik?' }
],
practicalTasks: [
{ title: 'Fehlercluster', text: 'Notiere nach der Übung drei Sätze, bei denen du gezögert hast, und wiederhole sie laut.' }
]
},
'Woche 5 - Checkpoint': {
learningGoals: [
'Die Inhalte von Woche 5 diagnostisch sichern.',
'Besuch, Termine, Fragen und Gesundheit in freien Antworten kombinieren.',
'Schwache Muster für die nächste Wiederholungsphase markieren.',
'Eine kurze Besuchs- oder Pflegesituation ohne Vorlage lösen.'
],
corePatterns: [
{ target: 'Sulod lang.', gloss: 'Komm ruhig herein.' },
{ target: 'Unsa orasa?', gloss: 'Um wie viel Uhr?' },
{ target: 'Dili ko pwede karon.', gloss: 'Ich kann heute nicht.' },
{ target: 'Magpahuway sa.', gloss: 'Ruh dich erst einmal aus.' },
{ target: 'Sakit imong ulo?', gloss: 'Tut dein Kopf weh?' },
{ target: 'Kumusta ang biyahe?', gloss: 'Wie war die Reise?' },
{ target: 'Pwede ko mangutana?', gloss: 'Darf ich fragen?' },
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' }
],
speakingPrompts: [
{ title: 'Checkpoint-Szene', prompt: 'Löse eine Szene mit Besuch, Termin und Gesundheitsfrage.', cue: 'Sulod lang. Magkita ta ugma? Sakit imong ulo?' }
],
practicalTasks: [
{ title: 'Diagnose', text: 'Beantworte fünf freie Situationen und markiere alle Sätze, die nicht sofort kamen.' }
]
},
'Unterwegs & Transport': {
learningGoals: [
'Nach Weg, Haltestelle, Fahrpreis und Ziel fragen.',
'Sagen, wohin man fährt und wo man aussteigen möchte.',
'Transportfragen mit Orts- und Preiswissen aus früheren Wochen verbinden.',
'Eine einfache Fahrt sprachlich organisieren.'
],
corePatterns: [
{ target: 'Asa ang sakayan?', gloss: 'Wo ist die Haltestelle / Mitfahrstelle?' },
{ target: 'Moadto ko sa lungsod.', gloss: 'Ich fahre/gehe in die Stadt.' },
{ target: 'Hunong lang dinhi.', gloss: 'Bitte hier anhalten.' },
{ target: 'Pila ang plite?', gloss: 'Wie viel kostet die Fahrt?' },
{ target: 'Asa ko manaog?', gloss: 'Wo soll ich aussteigen?' },
{ target: 'Duol ra ba?', gloss: 'Ist es nah?' },
{ target: 'Layo pa ba?', gloss: 'Ist es noch weit?' },
{ target: 'Padulong ko sa balay.', gloss: 'Ich bin auf dem Weg nach Hause.' }
],
grammarFocus: [
{ title: 'moadto und padulong', text: 'moadto betont das Hingehen/Fahren, padulong beschreibt die Richtung.', example: 'Moadto ko sa lungsod. Padulong ko sa balay.' },
{ title: 'dinhi', text: 'dinhi bedeutet hier und ist beim Aussteigen oder Anhalten praktisch.', example: 'Hunong lang dinhi.' }
],
speakingPrompts: [
{ title: 'Weg erfragen', prompt: 'Frage nach Haltestelle, Preis und Ausstieg.', cue: 'Asa ang sakayan? Pila ang plite? Asa ko manaog?' },
{ title: 'Fahrt beschreiben', prompt: 'Sage, dass du in die Stadt und später nach Hause fährst.', cue: 'Moadto ko sa lungsod. Padulong ko sa balay.' }
],
practicalTasks: [
{ title: 'Transportdialog', text: 'Sprich eine kurze Fahrt: Ziel, Preis, Ausstieg, Dank.' }
]
},
'Wege & Verkehr': {
learningGoals: [
'Verkehrs- und Wegwörter aktiv abrufen.',
'Route, Fahrzeug und Halt in einfachen Sätzen beschreiben.',
'Frühere Richtungswörter mit Transport verbinden.',
'Eine kurze Route mit mindestens vier Stationen erklären.'
],
corePatterns: [
{ target: 'sakyanan', gloss: 'Fahrzeug / Auto' },
{ target: 'jeepney', gloss: 'Jeepney' },
{ target: 'dalan', gloss: 'Straße / Weg' },
{ target: 'hunong', gloss: 'Halt / anhalten' },
{ target: 'biyahe', gloss: 'Reise / Fahrt' },
{ target: 'tuo', gloss: 'rechts' },
{ target: 'wala', gloss: 'links' },
{ target: 'diretso', gloss: 'geradeaus' },
{ target: 'Duol ra.', gloss: 'Es ist nur nah.' }
],
speakingPrompts: [
{ title: 'Route beschreiben', prompt: 'Beschreibe eine einfache Route mit geradeaus, rechts und links.', cue: 'Diretso. Tuo. Wala. Duol ra.' }
],
practicalTasks: [
{ title: 'Wegskizze', text: 'Erkläre den Weg von Zuhause zur Haltestelle in vier kurzen Sätzen.' }
]
},
'Arbeit & Aufgaben': {
learningGoals: [
'Über Arbeit, Haushaltspflichten und To-dos sprechen.',
'Sagen, was man noch erledigen muss und ob man fertig ist.',
'Hilfe anbieten oder erbitten.',
'Eine Tagesaufgabe mit Zeit und Grund erklären.'
],
corePatterns: [
{ target: 'Naa koy trabaho karon.', gloss: 'Ich habe heute Arbeit.' },
{ target: 'Aduna koy buhaton.', gloss: 'Ich habe etwas zu erledigen.' },
{ target: 'Human na ko.', gloss: 'Ich bin fertig.' },
{ target: 'Wala pa ko nahuman.', gloss: 'Ich bin noch nicht fertig.' },
{ target: 'Tabang ta.', gloss: 'Lass uns helfen.' },
{ target: 'Tabangi ko, palihug.', gloss: 'Hilf mir bitte.' },
{ target: 'Unahon nako ni.', gloss: 'Das mache ich zuerst.' },
{ target: 'Human sa paniudto.', gloss: 'Nach dem Mittagessen.' }
],
grammarFocus: [
{ title: 'naa koy / aduna koy', text: 'Beide Muster drücken aus, dass du etwas hast oder erledigen musst.', example: 'Naa koy trabaho. Aduna koy buhaton.' },
{ title: 'human na vs. wala pa', text: 'human na zeigt fertig, wala pa zeigt noch nicht.', example: 'Human na ko. Wala pa ko nahuman.' }
],
speakingPrompts: [
{ title: 'Tagesaufgabe', prompt: 'Erkläre, was du heute erledigen musst, was zuerst kommt und wann du fertig bist.', cue: 'Aduna koy buhaton. Unahon nako ni. Human na ko unya.' }
],
practicalTasks: [
{ title: 'To-do laut', text: 'Sprich drei Aufgaben: eine erledigt, eine offen, eine mit Hilfe.' }
]
},
'Tätigkeiten & Organisation': {
learningGoals: [
'Organisationswortschatz für Alltag und Haushalt erweitern.',
'Listen, Aufgaben und Hilfe in kurzen Sätzen verwenden.',
'Zwischen Tätigkeit, Person und Reihenfolge unterscheiden.',
'Eine einfache To-do-Liste auf Bisaya sprechen.'
],
corePatterns: [
{ target: 'trabaho', gloss: 'Arbeit' },
{ target: 'buluhaton', gloss: 'Aufgabe / Tätigkeit' },
{ target: 'lista', gloss: 'Liste' },
{ target: 'tabang', gloss: 'Hilfe' },
{ target: 'una', gloss: 'zuerst' },
{ target: 'sunod', gloss: 'als Nächstes' },
{ target: 'human', gloss: 'fertig / danach' },
{ target: 'Kinsa ang motabang?', gloss: 'Wer hilft?' },
{ target: 'Unsa ang sunod?', gloss: 'Was kommt als Nächstes?' }
],
speakingPrompts: [
{ title: 'Organisation', prompt: 'Sage, was zuerst kommt, was danach kommt und wer hilft.', cue: 'Una kini. Sunod kana. Kinsa ang motabang?' }
],
practicalTasks: [
{ title: 'Mini-Liste', text: 'Erstelle mündlich eine Liste mit drei Aufgaben und einer Bitte um Hilfe.' }
]
},
'Freies Gespräch - Familie & Alltag': {
learningGoals: [
'Frühere Themen ohne enge Führung kombinieren.',
'Längere Alltagsantworten mit Familie, Zuhause, Befinden und Plan aufbauen.',
'Bei Unsicherheit Reparaturstrategien verwenden statt abzubrechen.',
'Mindestens fünf zusammenhängende Sätze frei sprechen.'
],
corePatterns: [
{ target: 'Maayo ra ko karon.', gloss: 'Mir geht es heute okay.' },
{ target: 'Naa mi sa balay.', gloss: 'Wir sind zuhause.' },
{ target: 'Nikaon na mi.', gloss: 'Wir haben schon gegessen.' },
{ target: 'Aduna koy buhaton unya.', gloss: 'Ich habe später etwas zu erledigen.' },
{ target: 'Magkita mi unya.', gloss: 'Wir sehen uns später.' },
{ target: 'Gimingaw ko nimo.', gloss: 'Ich vermisse dich.' },
{ target: 'Wala ko kasabot, palihug balik.', gloss: 'Ich verstehe nicht, bitte wiederhole es.' },
{ target: 'Amping kanunay.', gloss: 'Pass immer auf dich auf.' }
],
grammarFocus: [
{ title: 'mi für wir', text: 'mi nutzt du, wenn du über wir sprichst und die angesprochene Person nicht einschließt.', example: 'Naa mi sa balay. Nikaon na mi.' },
{ title: 'Freies Sprechen mit Ankern', text: 'Nutze bekannte Satzanker und füge nur kleine neue Teile hinzu.', example: 'Maayo ra ko karon. Aduna koy buhaton unya.' }
],
speakingPrompts: [
{ title: 'Fünf Sätze frei', prompt: 'Erzähle von deinem Tag mit Befinden, Zuhause, Essen, Plan und Nähe.', cue: 'Maayo ra ko karon. Naa mi sa balay. Nikaon na mi. Aduna koy buhaton unya. Gimingaw ko nimo.' }
],
practicalTasks: [
{ title: 'Freie Aufnahme', text: 'Sprich 30 Sekunden frei und verwende mindestens einen Reparatursatz, wenn du stockst.' }
]
},
'Spiralwiederholung - Wochen 1 bis 4': {
learningGoals: [
'Grundlagen aus den ersten vier Wochen breit reaktivieren.',
'Begrüßung, Essen, Zeit, Preise, Gefühle und Gesundheit mischen.',
'Alte Muster in neuen Kontexten wiederfinden.',
'Langzeitabruf stärker gewichten als neues Material.'
],
corePatterns: [
{ target: 'Kumusta ka?', gloss: 'Wie geht es dir?' },
{ target: 'Maayong gabii.', gloss: 'Guten Abend.' },
{ target: 'Nikaon na ka?', gloss: 'Hast du schon gegessen?' },
{ target: 'Tagpila ni?', gloss: 'Wie viel kostet das?' },
{ target: 'Karon', gloss: 'jetzt / heute' },
{ target: 'Ugma', gloss: 'morgen' },
{ target: 'Nalipay ko.', gloss: 'Ich freue mich / bin glücklich.' },
{ target: 'Sakit akong ulo.', gloss: 'Mein Kopf tut weh.' },
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' }
],
speakingPrompts: [
{ title: 'Blockmix', prompt: 'Baue aus Begrüßung, Essen, Preis und Gesundheit eine Alltagsszene.', cue: 'Kumusta ka? Nikaon na ka? Tagpila ni? Sakit akong ulo.' }
],
practicalTasks: [
{ title: 'Langzeitabruf', text: 'Ziehe zehn alte Wörter/Sätze zufällig und bilde daraus fünf kurze Dialogantworten.' }
]
},
'Konflikte & Missverständnisse': {
learningGoals: [
'Missverständnisse höflich ansprechen und entschärfen.',
'Um Wiederholung, langsameres Sprechen oder Erklärung bitten.',
'Eigene Unsicherheit ausdrücken, ohne das Gespräch abzubrechen.',
'Eine kleine heikle Situation ruhig reparieren.'
],
corePatterns: [
{ target: 'Wala ko kasabot.', gloss: 'Ich verstehe nicht.' },
{ target: 'Hinay-hinay lang.', gloss: 'Bitte langsam.' },
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' },
{ target: 'Pwede nato istoryahan?', gloss: 'Können wir darüber sprechen?' },
{ target: 'Pasayloa ko.', gloss: 'Entschuldige mich.' },
{ target: 'Dili mao akong pasabot.', gloss: 'Das meinte ich nicht.' },
{ target: 'Sige lang.', gloss: 'Schon okay.' },
{ target: 'Salamat sa pagsabot.', gloss: 'Danke fürs Verständnis.' }
],
grammarFocus: [
{ title: 'Reparieren statt abbrechen', text: 'Ein Reparatursatz hält das Gespräch offen, auch wenn du etwas nicht verstanden hast.', example: 'Wala ko kasabot. Palihug ka mubalik?' },
{ title: 'Weich entschärfen', text: 'Sige lang und Salamat sa pagsabot helfen, Spannung aus dem Gespräch zu nehmen.', example: 'Sige lang. Salamat sa pagsabot.' }
],
speakingPrompts: [
{ title: 'Missverständnis lösen', prompt: 'Sage, dass du nicht verstanden hast, bitte um Wiederholung und entschuldige dich.', cue: 'Wala ko kasabot. Palihug ka mubalik? Pasayloa ko.' },
{ title: 'Heikle Korrektur', prompt: 'Sage, dass du etwas nicht so gemeint hast, und danke fürs Verständnis.', cue: 'Dili mao akong pasabot. Salamat sa pagsabot.' }
],
practicalTasks: [
{ title: 'Konfliktanker', text: 'Übe fünf Reparatursätze, bis du sie ohne Lesen sagen kannst.' }
]
},
'Abschlusstest - Schnellstart': {
learningGoals: [
'Aktiven Grundwortschatz aus dem gesamten 6-Wochen-Schnellstart prüfen.',
'Wörter und kurze Sätze in beide Richtungen abrufen.',
'Ähnliche Muster sicher unterscheiden.',
'Schwächen für die nächste SRS-Wiederholung markieren.'
],
corePatterns: [
{ target: 'Kumusta', gloss: 'Wie geht es / Hallo' },
{ target: 'bisita', gloss: 'Besuch / Gast' },
{ target: 'tambal', gloss: 'Medizin' },
{ target: 'plite', gloss: 'Fahrpreis' },
{ target: 'trabaho', gloss: 'Arbeit' },
{ target: 'buluhaton', gloss: 'Aufgabe' },
{ target: 'dalan', gloss: 'Straße / Weg' },
{ target: 'pamilya', gloss: 'Familie' },
{ target: 'Gimingaw ko nimo.', gloss: 'Ich vermisse dich.' },
{ target: 'Palihug ka mubalik?', gloss: 'Kannst du das bitte wiederholen?' }
],
speakingPrompts: [
{ title: 'Aktiver Wortschatz', prompt: 'Wähle acht Wörter und bilde vier Alltagssätze daraus.', cue: 'bisita -> Sulod lang. tambal -> Niinom ka og tambal?' }
],
practicalTasks: [
{ title: 'Beide Richtungen', text: 'Übersetze zehn Einheiten Deutsch -> Bisaya und Bisaya -> Deutsch.' }
]
},
'Abschlussprüfung - Schnellstart': {
learningGoals: [
'Den 6-Wochen-Schnellstart in Dialog, Abruf und freier Anwendung prüfen.',
'Besuch, Gesundheit, Transport, Arbeit und Familie zusammenführen.',
'Mehrere Sätze ohne starre Vorlage produzieren.',
'Vor dem Übergang in die Alltagsphase stabile Grundfähigkeit zeigen.'
],
corePatterns: [
{ target: 'Sulod lang. Lingkod sa.', gloss: 'Komm herein. Setz dich erst einmal.' },
{ target: 'Magkita ta ugma. Unsa orasa?', gloss: 'Wir treffen uns morgen. Um wie viel Uhr?' },
{ target: 'Niinom ka og tambal?', gloss: 'Hast du Medizin genommen?' },
{ target: 'Asa ang sakayan?', gloss: 'Wo ist die Haltestelle?' },
{ target: 'Pila ang plite?', gloss: 'Wie viel kostet die Fahrt?' },
{ target: 'Aduna koy buhaton karon.', gloss: 'Ich habe heute etwas zu erledigen.' },
{ target: 'Wala ko kasabot. Hinay-hinay lang.', gloss: 'Ich verstehe nicht. Bitte langsam.' },
{ target: 'Amping kanunay.', gloss: 'Pass immer auf dich auf.' }
],
speakingPrompts: [
{ title: 'Große Alltagsszene', prompt: 'Verbinde Besuch, Termin, Gesundheit und Transport in einem Mini-Dialog.', cue: 'Sulod lang. Magkita ta ugma? Niinom ka og tambal? Asa ang sakayan?' },
{ title: 'Freie Antwort', prompt: 'Antworte in mindestens fünf Sätzen auf: Was machst du heute und wen triffst du?', cue: 'Aduna koy buhaton karon. Magkita ta ugma. Amping kanunay.' }
],
practicalTasks: [
{ title: 'Abschlussaufnahme', text: 'Sprich eine 45-Sekunden-Szene mit mindestens sechs Kernmustern aus Woche 1-6.' }
]
},
'Kulturelle Vertiefung im Familienalltag': {
learningGoals: [
'Sprache, Familiennähe und Respekt kulturell einordnen.',
'Indirekte Kommunikation als soziale Strategie verstehen.',
'Nähe, Hilfe und Ablehnung passend formulieren.',
'Kulturelles Verständnis in konkrete Antwortwahl übertragen.'
],
corePatterns: [
{ target: 'pakikisama', gloss: 'soziale Harmonie / gutes Miteinander' },
{ target: 'respeto', gloss: 'Respekt' },
{ target: 'amping', gloss: 'Pass auf dich auf' },
{ target: 'palihug', gloss: 'bitte' },
{ target: 'Dili lang sa karon.', gloss: 'Jetzt lieber nicht.' },
{ target: 'Sige lang.', gloss: 'Schon okay.' },
{ target: 'Tabang ta.', gloss: 'Lass uns helfen.' },
{ target: 'Salamat sa pagsabot.', gloss: 'Danke fürs Verständnis.' }
],
grammarFocus: [
{ title: 'Indirekt ist oft höflicher', text: 'In Familien- und Besuchskontexten kann eine weiche Formulierung natürlicher sein als ein direktes Nein.', example: 'Dili lang sa karon. Sunod na lang.' },
{ title: 'Sprache als Beziehungspflege', text: 'Viele Formeln transportieren nicht nur Inhalt, sondern Fürsorge und Nähe.', example: 'Amping kanunay. Tabang ta.' }
],
speakingPrompts: [
{ title: 'Kulturell passend reagieren', prompt: 'Lehne eine Einladung weich ab und bedanke dich fürs Verständnis.', cue: 'Dili lang sa karon. Salamat sa pagsabot.' },
{ title: 'Hilfe anbieten', prompt: 'Biete Hilfe an, ohne aufdringlich zu klingen.', cue: 'Tabang ta. Sige lang.' }
],
practicalTasks: [
{ title: 'Antwortwahl', text: 'Vergleiche drei direkte deutsche Antworten und formuliere sie weicher auf Bisaya.' }
]
}
};
export const BISAYA_PHASE3_LESSONS = [
{ week: 5, day: 1, num: 44, type: 'conversation', title: 'Besuch & Gastfreundschaft', desc: 'Besuch empfangen, hereinbitten, etwas anbieten und nach der Reise fragen', targetMin: 24, targetScore: 80, review: false, cultural: 'Gastfreundschaft ist im philippinischen Familienalltag zentral und wird oft durch konkrete Fürsorge gezeigt.' },
{ week: 5, day: 1, num: 45, type: 'vocab', title: 'Besuch & Haushalt', desc: 'Haus-, Besuchs- und Haushaltswörter in kurzen Ortssätzen verwenden', targetMin: 22, targetScore: 85, review: true, cultural: null },
{ week: 5, day: 2, num: 46, type: 'grammar', title: 'Fragen im Alltag vertiefen', desc: 'Rückfragen, Folgefragen, Bedeutung und höfliches Nachhaken sicher kombinieren', targetMin: 26, targetScore: 78, review: true, cultural: null },
{ week: 5, day: 2, num: 47, type: 'conversation', title: 'Termine & Verabredungen', desc: 'Treffen planen, Uhrzeiten absprechen, zusagen und weich verschieben', targetMin: 24, targetScore: 80, review: false, cultural: null },
{ week: 5, day: 3, num: 48, type: 'review', title: 'Woche 5 - Intensivwiederholung I', desc: 'Besuch, Familie, Fürsorge und Terminplanung in schnellen Rollenwechseln mischen', targetMin: 34, targetScore: 82, review: false, cultural: null },
{ week: 5, day: 3, num: 49, type: 'vocab', title: 'Spiralwiederholung - Familie & Fürsorge', desc: 'Alte Familien- und Fürsorgemuster aktiv reaktivieren und in neue Szenen übertragen', targetMin: 24, targetScore: 85, review: true, cultural: null },
{ week: 5, day: 4, num: 50, type: 'conversation', title: 'Gesundheit im Alltag', desc: 'Beschwerden erfragen, Ruhe/Wasser/Medizin anbieten und nach Besserung fragen', targetMin: 26, targetScore: 80, review: false, cultural: null },
{ week: 5, day: 4, num: 51, type: 'vocab', title: 'Medikamente & Beschwerden', desc: 'Symptome, Körperwörter und Hilfewortschatz in Fürsorgesätzen verwenden', targetMin: 24, targetScore: 85, review: true, cultural: null },
{ week: 5, day: 5, num: 52, type: 'review', title: 'Woche 5 - Intensivwiederholung II', desc: 'Gesundheit, Besuch, Fragen und Terminplanung unter Abrufdruck kontrastieren', targetMin: 34, targetScore: 82, review: false, cultural: null },
{ week: 5, day: 5, num: 53, type: 'vocab', title: 'Woche 5 - Checkpoint', desc: 'Diagnostischer Checkpoint zu Besuch, Termin, Frageketten und Gesundheit', targetMin: 24, targetScore: 84, review: true, cultural: null },
{ week: 6, day: 1, num: 54, type: 'conversation', title: 'Unterwegs & Transport', desc: 'Nach Weg, Haltestelle, Fahrpreis, Ziel und Ausstieg fragen', targetMin: 26, targetScore: 80, review: false, cultural: null },
{ week: 6, day: 1, num: 55, type: 'vocab', title: 'Wege & Verkehr', desc: 'Verkehrs-, Richtungs- und Bewegungswortschatz in einfachen Routen verwenden', targetMin: 24, targetScore: 85, review: true, cultural: null },
{ week: 6, day: 2, num: 56, type: 'conversation', title: 'Arbeit & Aufgaben', desc: 'Über Arbeit, Pflichten, Reihenfolge, Fertigsein und Hilfe sprechen', targetMin: 26, targetScore: 80, review: false, cultural: null },
{ week: 6, day: 2, num: 57, type: 'vocab', title: 'Tätigkeiten & Organisation', desc: 'Aufgaben, Listen, Reihenfolge und Hilfe als To-do-Sprache festigen', targetMin: 24, targetScore: 85, review: true, cultural: null },
{ week: 6, day: 3, num: 58, type: 'conversation', title: 'Freies Gespräch - Familie & Alltag', desc: 'Fünf zusammenhängende Sätze zu Familie, Zuhause, Befinden und Tagesplan sprechen', targetMin: 30, targetScore: 78, review: false, cultural: null },
{ week: 6, day: 3, num: 59, type: 'review', title: 'Spiralwiederholung - Wochen 1 bis 4', desc: 'Begrüßung, Essen, Zeit, Preise, Gefühle und Gesundheit als Langzeitabruf mischen', targetMin: 34, targetScore: 84, review: false, cultural: null },
{ week: 6, day: 4, num: 60, type: 'conversation', title: 'Konflikte & Missverständnisse', desc: 'Missverständnisse höflich reparieren, um Wiederholung bitten und Spannung entschärfen', targetMin: 26, targetScore: 80, review: false, cultural: 'Ruhiger, indirekter Umgang hilft in heiklen Gesprächen oft mehr als direkte Korrektur.' },
{ week: 6, day: 4, num: 61, type: 'vocab', title: 'Abschlusstest - Schnellstart', desc: 'Aktiver Wortschatztest über den 6-Wochen-Schnellstart in beide Richtungen', targetMin: 24, targetScore: 84, review: true, cultural: null },
{ week: 6, day: 5, num: 62, type: 'review', title: 'Abschlussprüfung - Schnellstart', desc: 'Große Abschlussprüfung mit Besuch, Gesundheit, Transport, Arbeit und freier Anwendung', targetMin: 38, targetScore: 84, review: false, cultural: 'Die Abschlussphase prüft Verständlichkeit und Alltagsfähigkeit, nicht perfekte Grammatik.' },
{ week: 6, day: 5, num: 63, type: 'culture', title: 'Kulturelle Vertiefung im Familienalltag', desc: 'Respekt, Nähe, indirekte Kommunikation und Hilfe im Familienalltag einordnen', targetMin: 22, targetScore: 0, review: false, cultural: 'Sprache, Respekt und Familienrollen sind eng miteinander verbunden.' }
];

File diff suppressed because it is too large Load Diff

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