5 Commits

Author SHA1 Message Date
Torsten Schulz (local)
d74f7b852b Refactor proposal generation in FalukantService to improve character selection logic
- Removed the tracking of used character IDs and streamlined the exclusion of characters already proposed or currently active as directors.
- Enhanced logging for SQL queries and fallback mechanisms to ensure better visibility during character selection.
- Implemented a more efficient approach to gather and process character knowledge for proposal creation, ensuring accurate average calculations.
- Improved error handling to provide clearer feedback when no eligible characters are found.
2026-01-12 08:33:26 +01:00
Torsten Schulz (local)
92d6b15c3f Enhance proposal generation logic in FalukantService to prevent duplicate character usage
- Introduced a mechanism to track used character IDs, ensuring that previously proposed characters are excluded from future proposals.
- Added error handling and logging for scenarios where no eligible characters are found, improving traceability and user feedback.
- Implemented a fallback to include newer characters if older ones are unavailable, enhancing the robustness of the proposal generation process.
2026-01-12 08:24:00 +01:00
Torsten Schulz (local)
91f59062f5 Update BranchView to refresh active tab data on tab change and modify 3D model for female toddler character
- Enhanced the activeTab watcher to refresh data only when the selected branch changes and the tab is switched.
- Introduced a new refreshActiveTab method to load data for the currently active tab, improving data management and user experience.
- Updated the female toddler 3D model file for better integration in the application.
2026-01-12 08:07:50 +01:00
Torsten Schulz (local)
1674086c73 Enhance partner search and gift loading functionality in FalukantService and FamilyView
- Added detailed logging for partner search criteria and results in FalukantService to improve debugging and traceability.
- Refactored partner search logic to use a dynamic where clause for better readability and maintainability.
- Implemented error handling in FamilyView for gift loading, ensuring an empty array is set on failure to load gifts, enhancing user experience.
2026-01-09 14:28:01 +01:00
Torsten Schulz (local)
5ddb099f5a Add 3D character model integration and update dependencies
- Introduced a new CharacterModel3D component for rendering 3D character models in OverviewView.
- Updated package.json and package-lock.json to include 'three' library for 3D graphics support.
- Enhanced Vite configuration to allow access to external files and ensure proper handling of GLB/GLTF assets.
- Improved layout and styling in OverviewView for better visualization of character and avatar.
2026-01-09 13:29:32 +01:00
337 changed files with 15277 additions and 45390 deletions

7
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import socialnetworkRouter from './routers/socialnetworkRouter.js';
import forumRouter from './routers/forumRouter.js'; import forumRouter from './routers/forumRouter.js';
import falukantRouter from './routers/falukantRouter.js'; import falukantRouter from './routers/falukantRouter.js';
import friendshipRouter from './routers/friendshipRouter.js'; import friendshipRouter from './routers/friendshipRouter.js';
import modelsProxyRouter from './routers/modelsProxyRouter.js';
import blogRouter from './routers/blogRouter.js'; import blogRouter from './routers/blogRouter.js';
import match3Router from './routers/match3Router.js'; import match3Router from './routers/match3Router.js';
import taxiRouter from './routers/taxiRouter.js'; import taxiRouter from './routers/taxiRouter.js';
@@ -20,9 +19,6 @@ import taxiMapRouter from './routers/taxiMapRouter.js';
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js'; import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
import termineRouter from './routers/termineRouter.js'; import termineRouter from './routers/termineRouter.js';
import vocabRouter from './routers/vocabRouter.js'; import vocabRouter from './routers/vocabRouter.js';
import dashboardRouter from './routers/dashboardRouter.js';
import newsRouter from './routers/newsRouter.js';
import calendarRouter from './routers/calendarRouter.js';
import cors from 'cors'; import cors from 'cors';
import './jobs/sessionCleanup.js'; import './jobs/sessionCleanup.js';
@@ -36,19 +32,6 @@ const app = express();
// - LOG_ALL_REQ=1: Logge alle Requests // - LOG_ALL_REQ=1: Logge alle Requests
const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1'; const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1';
const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10); const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10);
const defaultCorsOrigins = [
'http://localhost:3000',
'http://localhost:5173',
'http://127.0.0.1:3000',
'http://127.0.0.1:5173'
];
const corsOrigins = (process.env.CORS_ORIGINS || process.env.FRONTEND_URL || '')
.split(',')
.map((origin) => origin.trim())
.filter(Boolean);
const effectiveCorsOrigins = corsOrigins.length > 0 ? corsOrigins : defaultCorsOrigins;
const corsAllowAll = process.env.CORS_ALLOW_ALL === '1';
app.use((req, res, next) => { app.use((req, res, next) => {
const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex')); const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex'));
req.reqId = reqId; req.reqId = reqId;
@@ -64,26 +47,15 @@ app.use((req, res, next) => {
}); });
const corsOptions = { const corsOptions = {
origin(origin, callback) { origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
if (!origin) {
return callback(null, true);
}
if (corsAllowAll || effectiveCorsOrigins.includes(origin)) {
return callback(null, true);
}
return callback(null, false);
},
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'userid', 'authcode', 'userId', 'authCode'], allowedHeaders: ['Content-Type', 'Authorization', 'userId', 'authCode'],
credentials: true, credentials: true,
preflightContinue: false, preflightContinue: false,
optionsSuccessStatus: 204 optionsSuccessStatus: 204
}; };
app.use(cors(corsOptions)); app.use(cors(corsOptions));
app.options('*', cors(corsOptions));
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);
@@ -102,22 +74,11 @@ app.use('/api/vocab', vocabRouter);
app.use('/api/forum', forumRouter); app.use('/api/forum', forumRouter);
app.use('/api/falukant', falukantRouter); app.use('/api/falukant', falukantRouter);
app.use('/api/friendships', friendshipRouter); app.use('/api/friendships', friendshipRouter);
app.use('/api/models', modelsProxyRouter);
app.use('/api/blog', blogRouter); app.use('/api/blog', blogRouter);
app.use('/api/termine', termineRouter); app.use('/api/termine', termineRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/news', newsRouter);
app.use('/api/calendar', calendarRouter);
// Serve frontend SPA for non-API routes to support history mode clean URLs // Serve frontend SPA for non-API routes to support history mode clean URLs
// /models/* nicht statisch ausliefern nur über /api/models (Proxy mit Komprimierung)
const frontendDir = path.join(__dirname, '../frontend'); const frontendDir = path.join(__dirname, '../frontend');
app.use((req, res, next) => {
if (req.path.startsWith('/models/')) {
return res.status(404).send('Use /api/models/ for 3D models (optimized).');
}
next();
});
app.use(express.static(path.join(frontendDir, 'dist'))); app.use(express.static(path.join(frontendDir, 'dist')));
app.get(/^\/(?!api\/).*/, (req, res) => { app.get(/^\/(?!api\/).*/, (req, res) => {
res.sendFile(path.join(frontendDir, 'dist', 'index.html')); res.sendFile(path.join(frontendDir, 'dist', 'index.html'));

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
{ {
"host": "localhost", "host": "localhost",
"port": 1236 "port": 1235
} }

View File

@@ -12,51 +12,25 @@ const productionEnvPath = '/opt/yourpart/backend/.env';
const localEnvPath = path.resolve(__dirname, '../.env'); const localEnvPath = path.resolve(__dirname, '../.env');
let envPath = localEnvPath; // Fallback let envPath = localEnvPath; // Fallback
let usingProduction = false;
if (fs.existsSync(productionEnvPath)) { if (fs.existsSync(productionEnvPath)) {
// Prüfe Lesbarkeit bevor wir versuchen, sie zu laden envPath = productionEnvPath;
try { console.log('[env] Lade Produktions-.env:', productionEnvPath);
fs.accessSync(productionEnvPath, fs.constants.R_OK);
envPath = productionEnvPath;
usingProduction = true;
console.log('[env] Produktions-.env gefunden und lesbar:', productionEnvPath);
} catch (err) {
console.warn('[env] Produktions-.env vorhanden, aber nicht lesbar - verwende lokale .env stattdessen:', productionEnvPath);
console.warn('[env] Fehler:', err && err.message);
envPath = localEnvPath;
}
} else { } else {
console.log('[env] Produktions-.env nicht gefunden, lade lokale .env:', localEnvPath); console.log('[env] Lade lokale .env:', localEnvPath);
} }
// Lade .env-Datei (robust gegen Fehler) // Lade .env-Datei
console.log('[env] Versuche .env zu laden von:', envPath); console.log('[env] Versuche .env zu laden von:', envPath);
console.log('[env] Datei existiert:', fs.existsSync(envPath)); console.log('[env] Datei existiert:', fs.existsSync(envPath));
let result; console.log('[env] Datei lesbar:', fs.accessSync ? (() => { try { fs.accessSync(envPath, fs.constants.R_OK); return true; } catch { return false; } })() : 'unbekannt');
try {
result = dotenv.config({ path: envPath }); const result = dotenv.config({ path: envPath });
if (result.error) { if (result.error) {
console.warn('[env] Konnte .env nicht laden:', result.error.message); console.warn('[env] Konnte .env nicht laden:', result.error.message);
console.warn('[env] Fehler-Details:', result.error); console.warn('[env] Fehler-Details:', result.error);
} else { } else {
console.log('[env] .env erfolgreich geladen von:', envPath, usingProduction ? '(production)' : '(local)'); console.log('[env] .env erfolgreich geladen von:', envPath);
console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {})); console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
}
} catch (err) {
// Sollte nicht passieren, aber falls dotenv intern eine Exception wirft (z.B. EACCES), fange sie ab
console.warn('[env] Unerwarteter Fehler beim Laden der .env:', err && err.message);
console.warn('[env] Stack:', err && err.stack);
if (envPath !== localEnvPath && fs.existsSync(localEnvPath)) {
console.log('[env] Versuche stattdessen lokale .env:', localEnvPath);
try {
result = dotenv.config({ path: localEnvPath });
if (!result.error) {
console.log('[env] Lokale .env erfolgreich geladen von:', localEnvPath);
}
} catch (err2) {
console.warn('[env] Konnte lokale .env auch nicht laden:', err2 && err2.message);
}
}
} }
// Debug: Zeige Redis-Konfiguration // Debug: Zeige Redis-Konfiguration

View File

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

View File

@@ -13,9 +13,6 @@ class ChatController {
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this); this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this); this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
this.getRoomList = this.getRoomList.bind(this); this.getRoomList = this.getRoomList.bind(this);
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
this.getOwnRooms = this.getOwnRooms.bind(this);
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
} }
async getMessages(req, res) { async getMessages(req, res) {
@@ -178,41 +175,6 @@ class ChatController {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
} }
async getRoomCreateOptions(req, res) {
try {
const options = await chatService.getRoomCreateOptions();
res.status(200).json(options);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getOwnRooms(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const rooms = await chatService.getOwnRooms(hashedUserId);
res.status(200).json(rooms);
} catch (error) {
const status = error.message === 'user_not_found' ? 404 : 500;
res.status(status).json({ error: error.message });
}
}
async deleteOwnRoom(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const roomId = Number.parseInt(req.params.id, 10);
if (!Number.isInteger(roomId) || roomId <= 0) {
return res.status(400).json({ error: 'invalid_room_id' });
}
await chatService.deleteOwnRoom(hashedUserId, roomId);
res.status(204).send();
} catch (error) {
const status = error.message === 'room_not_found_or_not_owner' || error.message === 'user_not_found' ? 404 : 500;
res.status(status).json({ error: error.message });
}
}
} }
export default ChatController; export default ChatController;

View File

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

View File

@@ -26,8 +26,6 @@ class FalukantController {
}, { successStatus: 201 }); }, { successStatus: 201 });
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId)); this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId)); this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId)); this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId)); this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
@@ -58,10 +56,6 @@ class FalukantController {
if (!page) page = 1; if (!page) page = 1;
return this.service.moneyHistory(userId, page, filter); return this.service.moneyHistory(userId, page, filter);
}); });
this.moneyHistoryGraph = this._wrapWithUser((userId, req) => {
const { range } = req.body || {};
return this.service.moneyHistoryGraph(userId, range || '24h');
});
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId)); this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
this.buyStorage = this._wrapWithUser((userId, req) => { this.buyStorage = this._wrapWithUser((userId, req) => {
const { branchId, amount, stockTypeId } = req.body; const { branchId, amount, stockTypeId } = req.body;
@@ -98,40 +92,18 @@ class FalukantController {
if (!result) throw { status: 404, message: 'No family data found' }; if (!result) throw { status: 404, message: 'No family data found' };
return result; return result;
}); });
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId)); this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId)); this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId)); this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.cancelWooing = this._wrapWithUser(async (userId) => {
try {
return await this.service.cancelWooing(userId);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'cancelTooSoon') {
throw { status: 412, message: 'cancelTooSoon', retryAt: e.meta?.retryAt };
}
throw e;
}
}, { successStatus: 202 });
this.getGifts = this._wrapWithUser((userId) => { this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId); console.log('🔍 getGifts called with userId:', userId);
return this.service.getGifts(userId); return this.service.getGifts(userId);
}); });
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId)); this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
this.sendGift = this._wrapWithUser(async (userId, req) => { this.sendGift = this._wrapWithUser((userId, req) => this.service.sendGift(userId, req.body.giftId));
try {
return await this.service.sendGift(userId, req.body.giftId);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooOften') {
throw { status: 412, message: 'tooOften', retryAt: e.meta?.retryAt };
}
throw e;
}
});
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId)); this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) =>
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201 });
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId)); this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId)); this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId)); this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
@@ -146,22 +118,17 @@ class FalukantController {
}, { successStatus: 201 }); }, { successStatus: 201 });
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId)); this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) => {
const { actionTypeId } = req.body;
return this.service.executeReputationAction(userId, actionTypeId);
}, { successStatus: 201 });
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId)); this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
this.baptise = this._wrapWithUser((userId, req) => { this.baptise = this._wrapWithUser((userId, req) => {
const { characterId: childId, firstName } = req.body; const { characterId: childId, firstName } = req.body;
return this.service.baptise(userId, childId, firstName); return this.service.baptise(userId, childId, firstName);
}); });
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
const { officeTypeId, regionId } = req.body;
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
});
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
const { applicationId, decision } = req.body;
return this.service.decideOnChurchApplication(userId, applicationId, decision);
});
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId)); this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
this.sendToSchool = this._wrapWithUser((userId, req) => { this.sendToSchool = this._wrapWithUser((userId, req) => {
@@ -177,16 +144,7 @@ class FalukantController {
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId)); this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId)); this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
this.healthActivity = this._wrapWithUser(async (userId, req) => { this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr));
try {
return await this.service.healthActivity(userId, req.body.measureTr);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
}
throw e;
}
});
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId)); this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId)); this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
@@ -204,13 +162,6 @@ class FalukantController {
} }
return this.service.getProductPriceInRegion(userId, productId, regionId); return this.service.getProductPriceInRegion(userId, productId, regionId);
}); });
this.getAllProductPricesInRegion = this._wrapWithUser((userId, req) => {
const regionId = parseInt(req.query.regionId, 10);
if (Number.isNaN(regionId)) {
throw new Error('regionId is required');
}
return this.service.getAllProductPricesInRegion(userId, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => { this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10); const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice); const currentPrice = parseFloat(req.query.currentPrice);
@@ -220,16 +171,6 @@ class FalukantController {
} }
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId); return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
}); });
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => {
const body = req.body || {};
const items = Array.isArray(body.items) ? body.items : [];
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
const valid = items.map(i => ({
productId: parseInt(i.productId, 10),
currentPrice: parseFloat(i.currentPrice)
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
});
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element)); this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId)); this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
@@ -237,7 +178,6 @@ class FalukantController {
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId)); this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size)); this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 }); this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId)); this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
this.searchUsers = this._wrapWithUser((userId, req) => { this.searchUsers = this._wrapWithUser((userId, req) => {
@@ -312,13 +252,7 @@ class FalukantController {
} catch (error) { } catch (error) {
console.error('Controller error:', error); console.error('Controller error:', error);
const status = error.status && typeof error.status === 'number' ? error.status : 500; const status = error.status && typeof error.status === 'number' ? error.status : 500;
// Wenn error ein Objekt mit status ist, alle Felder außer status übernehmen res.status(status).json({ error: error.message || 'Internal error' });
if (error && typeof error === 'object' && error.status && typeof error.status === 'number') {
const { status: errorStatus, ...errorData } = error;
res.status(errorStatus).json({ error: error.message || errorData.message || 'Internal error', ...errorData });
} else {
res.status(status).json({ error: error.message || 'Internal error' });
}
} }
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,13 +25,11 @@ function createServer() {
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined, ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
}); });
wss = new WebSocketServer({ server: httpsServer }); wss = new WebSocketServer({ server: httpsServer });
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
httpsServer.listen(PORT, '0.0.0.0', () => { httpsServer.listen(PORT, '0.0.0.0', () => {
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`); console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
}); });
} else { } else {
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0) wss = new WebSocketServer({ port: PORT });
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`); console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +0,0 @@
BEGIN;
ALTER TABLE chat.room
ADD COLUMN IF NOT EXISTS gender_restriction_id INTEGER,
ADD COLUMN IF NOT EXISTS min_age INTEGER,
ADD COLUMN IF NOT EXISTS max_age INTEGER,
ADD COLUMN IF NOT EXISTS password VARCHAR(255),
ADD COLUMN IF NOT EXISTS friends_of_owner_only BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS required_user_right_id INTEGER;
UPDATE chat.room
SET friends_of_owner_only = FALSE
WHERE friends_of_owner_only IS NULL;
ALTER TABLE chat.room
ALTER COLUMN friends_of_owner_only SET DEFAULT FALSE,
ALTER COLUMN friends_of_owner_only SET NOT NULL;
COMMIT;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,41 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
/**
* Vorteile pro Stand (Adelstitel).
* benefit_type: 'tax_share' | 'tax_exempt' | 'office_eligibility' | 'free_party_type' | 'reputation_bonus'
* parameters: JSONB, z.B. { officeTypeNames: [] }, { partyTypeIds: [] }, { minPercent: 5, maxPercent: 15 }
*/
class TitleBenefit extends Model {}
TitleBenefit.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
titleId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'title_id'
},
benefitType: {
type: DataTypes.STRING,
allowNull: false,
field: 'benefit_type'
},
parameters: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: {}
}
}, {
sequelize,
modelName: 'TitleBenefit',
tableName: 'title_benefit',
schema: 'falukant_type',
timestamps: false,
underscored: true
});
export default TitleBenefit;

View File

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

View File

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

View File

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

4322
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -15,8 +15,5 @@ router.post('/initOneToOne', authenticate, chatController.initOneToOne);
router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht
router.get('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte router.get('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte
router.get('/rooms', chatController.getRoomList); router.get('/rooms', chatController.getRoomList);
router.get('/room-create-options', authenticate, chatController.getRoomCreateOptions);
router.get('/my-rooms', authenticate, chatController.getOwnRooms);
router.delete('/my-rooms/:id', authenticate, chatController.deleteOwnRoom);
export default router; export default router;

View File

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

View File

@@ -11,7 +11,6 @@ router.get('/character/affect', falukantController.getCharacterAffect);
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName); router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
router.get('/name/randomlastname', falukantController.randomLastName); router.get('/name/randomlastname', falukantController.randomLastName);
router.get('/info', falukantController.getInfo); router.get('/info', falukantController.getInfo);
router.get('/dashboard-widget', falukantController.getDashboardWidget);
router.get('/branches/types', falukantController.getBranchTypes); router.get('/branches/types', falukantController.getBranchTypes);
router.get('/branches/:branch', falukantController.getBranch); router.get('/branches/:branch', falukantController.getBranch);
router.get('/branches', falukantController.getBranches); router.get('/branches', falukantController.getBranches);
@@ -29,7 +28,6 @@ router.get('/inventory/?:branchId', falukantController.getInventory);
router.post('/sell/all', falukantController.sellAllProducts); router.post('/sell/all', falukantController.sellAllProducts);
router.post('/sell', falukantController.sellProduct); router.post('/sell', falukantController.sellProduct);
router.post('/moneyhistory', falukantController.moneyHistory); router.post('/moneyhistory', falukantController.moneyHistory);
router.post('/moneyhistory/graph', falukantController.moneyHistoryGraph);
router.get('/storage/:branchId', falukantController.getStorage); router.get('/storage/:branchId', falukantController.getStorage);
router.post('/storage', falukantController.buyStorage); router.post('/storage', falukantController.buyStorage);
router.delete('/storage', falukantController.sellStorage); router.delete('/storage', falukantController.sellStorage);
@@ -39,13 +37,7 @@ router.post('/director/settings', falukantController.setSetting);
router.get('/director/:branchId', falukantController.getDirectorForBranch); router.get('/director/:branchId', falukantController.getDirectorForBranch);
router.get('/directors', falukantController.getAllDirectors); router.get('/directors', falukantController.getAllDirectors);
router.post('/directors', falukantController.updateDirector); router.post('/directors', falukantController.updateDirector);
// Legacy endpoint (wurde in einem Refactor entfernt, wird aber von WidgetTypes/Frontend erwartet)
// Liefert eine schlanke, frontend-kompatible Widget-Antwort (ohne hashedIds).
router.get('/dashboard-widget', falukantController.getDashboardWidget);
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal); router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
router.post('/family/cancel-wooing', falukantController.cancelWooing);
router.post('/family/set-heir', falukantController.setHeir); router.post('/family/set-heir', falukantController.setHeir);
router.get('/heirs/potential', falukantController.getPotentialHeirs); router.get('/heirs/potential', falukantController.getPotentialHeirs);
router.post('/heirs/select', falukantController.selectHeir); router.post('/heirs/select', falukantController.selectHeir);
@@ -54,8 +46,6 @@ router.get('/family/children', falukantController.getChildren);
router.post('/family/gift', falukantController.sendGift); router.post('/family/gift', falukantController.sendGift);
router.get('/family', falukantController.getFamily); router.get('/family', falukantController.getFamily);
router.get('/nobility/titels', falukantController.getTitlesOfNobility); router.get('/nobility/titels', falukantController.getTitlesOfNobility);
router.get('/reputation/actions', falukantController.getReputationActions);
router.post('/reputation/actions', falukantController.executeReputationAction);
router.get('/houses/types', falukantController.getHouseTypes); router.get('/houses/types', falukantController.getHouseTypes);
router.get('/houses/buyable', falukantController.getBuyableHouses); router.get('/houses/buyable', falukantController.getBuyableHouses);
router.get('/houses', falukantController.getUserHouse); router.get('/houses', falukantController.getUserHouse);
@@ -65,13 +55,10 @@ router.post('/houses', falukantController.buyUserHouse);
router.get('/party/types', falukantController.getPartyTypes); router.get('/party/types', falukantController.getPartyTypes);
router.post('/party', falukantController.createParty); router.post('/party', falukantController.createParty);
router.get('/party', falukantController.getParties); router.get('/party', falukantController.getParties);
router.get('/reputation/actions', falukantController.getReputationActions);
router.post('/reputation/actions', falukantController.executeReputationAction);
router.get('/family/notbaptised', falukantController.getNotBaptisedChildren); router.get('/family/notbaptised', falukantController.getNotBaptisedChildren);
router.post('/church/baptise', falukantController.baptise); router.post('/church/baptise', falukantController.baptise);
router.get('/church/overview', falukantController.getChurchOverview);
router.get('/church/positions/available', falukantController.getAvailableChurchPositions);
router.get('/church/applications/supervised', falukantController.getSupervisedApplications);
router.post('/church/positions/apply', falukantController.applyForChurchPosition);
router.post('/church/applications/decide', falukantController.decideOnChurchApplication);
router.get('/education', falukantController.getEducation); router.get('/education', falukantController.getEducation);
router.post('/education', falukantController.sendToSchool); router.post('/education', falukantController.sendToSchool);
router.get('/bank/overview', falukantController.getBankOverview); router.get('/bank/overview', falukantController.getBankOverview);
@@ -83,14 +70,13 @@ 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/open', falukantController.getOpenPolitics); router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections);
router.get('/politics/elections', falukantController.getElections); router.get('/politics/elections', falukantController.getElections);
router.post('/politics/elections', falukantController.vote); router.post('/politics/elections', falukantController.vote);
router.get('/politics/open', falukantController.getOpenPolitics);
router.post('/politics/open', falukantController.applyForElections);
router.get('/cities', falukantController.getRegions); router.get('/cities', falukantController.getRegions);
router.get('/products/price-in-region', falukantController.getProductPriceInRegion); router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion);
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities); router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
router.post('/products/prices-in-cities-batch', falukantController.getProductPricesInCitiesBatch);
router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes); router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes);
router.get('/vehicles/types', falukantController.getVehicleTypes); router.get('/vehicles/types', falukantController.getVehicleTypes);
router.post('/vehicles', falukantController.buyVehicles); router.post('/vehicles', falukantController.buyVehicles);

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import amqp from 'amqplib/callback_api.js'; import amqp from 'amqplib/callback_api.js';
import User from '../models/community/user.js';
import Room from '../models/chat/room.js';
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost'; const RABBITMQ_URL = 'amqp://localhost';
const QUEUE = 'oneToOne_messages'; const QUEUE = 'oneToOne_messages';
class ChatService { class ChatService {
@@ -13,37 +11,11 @@ class ChatService {
this.users = []; this.users = [];
this.randomChats = []; this.randomChats = [];
this.oneToOneChats = []; this.oneToOneChats = [];
this.channel = null;
this.amqpAvailable = false;
this.initRabbitMq();
}
initRabbitMq() {
amqp.connect(RABBITMQ_URL, (err, connection) => { amqp.connect(RABBITMQ_URL, (err, connection) => {
if (err) { if (err) throw err;
console.warn(`[chatService] RabbitMQ nicht erreichbar (${RABBITMQ_URL}) - fallback ohne Queue wird verwendet.`); connection.createChannel((err, channel) => {
return; if (err) throw err;
}
connection.on('error', (connectionError) => {
console.warn('[chatService] RabbitMQ-Verbindung fehlerhaft:', connectionError.message);
this.channel = null;
this.amqpAvailable = false;
});
connection.on('close', () => {
console.warn('[chatService] RabbitMQ-Verbindung geschlossen.');
this.channel = null;
this.amqpAvailable = false;
});
connection.createChannel((channelError, channel) => {
if (channelError) {
console.warn('[chatService] RabbitMQ-Channel konnte nicht erstellt werden:', channelError.message);
return;
}
this.channel = channel; this.channel = channel;
this.amqpAvailable = true;
channel.assertQueue(QUEUE, { durable: false }); channel.assertQueue(QUEUE, { durable: false });
}); });
}); });
@@ -144,14 +116,8 @@ class ChatService {
history: [messageBundle], history: [messageBundle],
}); });
} }
if (this.channel && this.amqpAvailable) { if (this.channel) {
try { this.channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(messageBundle)));
this.channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(messageBundle)));
} catch (error) {
console.warn('[chatService] sendToQueue fehlgeschlagen, Queue-Bridge vorübergehend deaktiviert:', error.message);
this.channel = null;
this.amqpAvailable = false;
}
} }
} }
@@ -182,66 +148,6 @@ class ChatService {
] ]
}); });
} }
async getRoomCreateOptions() {
const { default: UserRightType } = await import('../models/type/user_right.js');
const { default: InterestType } = await import('../models/type/interest.js');
const [rights, interests] = await Promise.all([
UserRightType.findAll({
attributes: ['id', 'title'],
order: [['id', 'ASC']]
}),
InterestType.findAll({
attributes: ['id', 'name'],
order: [['id', 'ASC']]
})
]);
return {
rights: rights.map((r) => ({ id: r.id, title: r.title })),
roomTypes: interests.map((i) => ({ id: i.id, name: i.name }))
};
}
async getOwnRooms(hashedUserId) {
const user = await User.findOne({
where: { hashedId: hashedUserId },
attributes: ['id']
});
if (!user) {
throw new Error('user_not_found');
}
return Room.findAll({
where: { ownerId: user.id },
attributes: ['id', 'title', 'isPublic', 'roomTypeId', 'ownerId'],
order: [['title', 'ASC']]
});
}
async deleteOwnRoom(hashedUserId, roomId) {
const user = await User.findOne({
where: { hashedId: hashedUserId },
attributes: ['id']
});
if (!user) {
throw new Error('user_not_found');
}
const deleted = await Room.destroy({
where: {
id: roomId,
ownerId: user.id
}
});
if (!deleted) {
throw new Error('room_not_found_or_not_owner');
}
return true;
}
} }
export default new ChatService(); export default new ChatService();

View File

@@ -2,10 +2,7 @@ import net from 'net';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = { host: 'localhost', port: 1235 };
host: process.env.CHAT_TCP_HOST || 'localhost',
port: Number.parseInt(process.env.CHAT_TCP_PORT || '1235', 10),
};
function loadBridgeConfig() { function loadBridgeConfig() {
try { try {

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,110 +2,38 @@
import { Server } from 'socket.io'; import { Server } from 'socket.io';
import amqp from 'amqplib/callback_api.js'; import amqp from 'amqplib/callback_api.js';
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost'; const RABBITMQ_URL = 'amqp://localhost';
const QUEUE = 'chat_messages'; const QUEUE = 'chat_messages';
const MAX_PENDING_MESSAGES = 500;
function routeMessage(io, message) {
if (!message || typeof message !== 'object') return;
if (message.socketId) {
io.to(message.socketId).emit('newMessage', message);
return;
}
if (message.recipientSocketId) {
io.to(message.recipientSocketId).emit('newMessage', message);
return;
}
if (message.roomId) {
io.to(String(message.roomId)).emit('newMessage', message);
return;
}
if (message.room) {
io.to(String(message.room)).emit('newMessage', message);
return;
}
io.emit('newMessage', message);
}
export function setupWebSocket(server) { export function setupWebSocket(server) {
const io = new Server(server); const io = new Server(server);
let channel = null;
let pendingMessages = [];
const flushPendingMessages = () => {
if (!channel || pendingMessages.length === 0) return;
const queued = pendingMessages;
pendingMessages = [];
for (const message of queued) {
try {
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
} catch (err) {
console.warn('[webSocketService] Flush fehlgeschlagen, Nachricht bleibt im Fallback:', err.message);
pendingMessages.unshift(message);
break;
}
}
};
amqp.connect(RABBITMQ_URL, (err, connection) => { amqp.connect(RABBITMQ_URL, (err, connection) => {
if (err) { if (err) throw err;
console.warn(`[webSocketService] RabbitMQ nicht erreichbar (${RABBITMQ_URL}) - WebSocket läuft ohne Queue-Bridge.`);
return;
}
connection.on('error', (connectionError) => { connection.createChannel((err, channel) => {
console.warn('[webSocketService] RabbitMQ-Verbindung fehlerhaft:', connectionError.message); if (err) throw err;
channel = null;
});
connection.on('close', () => {
console.warn('[webSocketService] RabbitMQ-Verbindung geschlossen.');
channel = null;
});
connection.createChannel((channelError, createdChannel) => {
if (channelError) {
console.warn('[webSocketService] RabbitMQ-Channel konnte nicht erstellt werden:', channelError.message);
return;
}
channel = createdChannel;
channel.assertQueue(QUEUE, { durable: false }); channel.assertQueue(QUEUE, { durable: false });
channel.consume(QUEUE, (msg) => {
if (!msg) return;
const message = JSON.parse(msg.content.toString());
routeMessage(io, message);
}, { noAck: true });
flushPendingMessages();
});
});
io.on('connection', (socket) => { io.on('connection', (socket) => {
console.log('Client connected via WebSocket'); console.log('Client connected via WebSocket');
socket.on('newMessage', (message) => { // Konsumiert Nachrichten aus RabbitMQ und sendet sie an den WebSocket-Client
if (channel) { channel.consume(QUEUE, (msg) => {
try { const message = JSON.parse(msg.content.toString());
io.emit('newMessage', message); // Broadcast an alle Clients
}, { noAck: true });
// Empfangt eine Nachricht vom WebSocket-Client und sendet sie an die RabbitMQ-Warteschlange
socket.on('newMessage', (message) => {
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message))); channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
} catch (err) { });
console.warn('[webSocketService] sendToQueue fehlgeschlagen, nutze In-Memory-Fallback:', err.message);
channel = null;
}
}
if (!channel) { socket.on('disconnect', () => {
pendingMessages.push(message); console.log('Client disconnected');
if (pendingMessages.length > MAX_PENDING_MESSAGES) { });
pendingMessages = pendingMessages.slice(-MAX_PENDING_MESSAGES); });
}
return;
}
});
socket.on('disconnect', () => {
console.log('Client disconnected');
}); });
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +0,0 @@
import { sequelize } from '../utils/sequelize.js';
import PromotionalGift from '../models/falukant/type/promotional_gift.js';
import PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
import PromotionalGiftCharacterTrait from '../models/falukant/predefine/promotional_gift_character_trait.js';
async function dump() {
try {
await sequelize.authenticate();
console.log('DB connected');
const gifts = await PromotionalGift.findAll({
include: [
{ model: PromotionalGiftMood, as: 'promotionalgiftmoods', attributes: ['moodId', 'suitability'], required: false },
{ model: PromotionalGiftCharacterTrait, as: 'characterTraits', attributes: ['traitId', 'suitability'], required: false }
]
});
console.log(`found ${gifts.length} gifts`);
for (const g of gifts) {
console.log('---');
console.log('id:', g.id, 'name:', g.name, 'raw value type:', typeof g.value, 'value:', g.value);
try {
const plain = g.get({ plain: true });
console.log('plain value:', JSON.stringify(plain));
} catch (e) {
console.log('could not stringify plain', e);
}
}
} catch (err) {
console.error('dump failed', err);
process.exit(2);
} finally {
await sequelize.close();
}
}
dump();

View File

@@ -1,61 +0,0 @@
import PromotionalGift from '../models/falukant/type/promotional_gift.js';
import { sequelize } from '../utils/sequelize.js';
// Mapping basierend auf initializeFalukantTypes.js
const seedValues = {
'Gold Coin': 100,
'Silk Scarf': 50,
'Exotic Perfume': 200,
'Crystal Pendant': 150,
'Leather Journal': 75,
'Fine Wine': 120,
'Artisan Chocolate': 40,
'Pearl Necklace': 300,
'Rare Painting': 500,
'Silver Watch': 250,
'Cat': 70,
'Dog': 150,
'Horse': 1000
};
async function repair() {
console.log('Repair promotional_gift values - starting');
try {
await sequelize.authenticate();
console.log('DB connection ok');
// Liste aller problematischen Einträge
const [rows] = await sequelize.query("SELECT id, name, value FROM falukant_type.promotional_gift WHERE value IS NULL OR value <= 0");
if (!rows.length) {
console.log('No invalid promotional_gift rows found. Nothing to do.');
return process.exit(0);
}
console.log(`Found ${rows.length} invalid promotional_gift rows:`);
for (const r of rows) console.log(` id=${r.id} name='${r.name}' value=${r.value}`);
// Update rows where we have a seed mapping
let updated = 0;
for (const r of rows) {
const seed = seedValues[r.name];
if (seed && Number(seed) > 0) {
await PromotionalGift.update({ value: seed }, { where: { id: r.id } });
console.log(` updated id=${r.id} name='${r.name}' -> value=${seed}`);
updated++;
} else {
console.warn(` no seed value for id=${r.id} name='${r.name}' - skipping`);
}
}
console.log(`Done. Updated ${updated} rows. Remaining invalid: `);
const [left] = await sequelize.query("SELECT id, name, value FROM falukant_type.promotional_gift WHERE value IS NULL OR value <= 0");
for (const l of left) console.log(` id=${l.id} name='${l.name}' value=${l.value}`);
console.log('If any remain, inspect and adjust manually.');
process.exit(0);
} catch (err) {
console.error('Repair failed:', err);
process.exit(2);
}
}
repair();

View File

@@ -1,20 +0,0 @@
function getGiftCostLocal(value, titleOfNobility, lowestTitleOfNobility) {
const val = Number(value) || 0;
const title = Number(titleOfNobility) || 1;
const lowest = Number(lowestTitleOfNobility) || 1;
const titleLevel = title - lowest + 1;
const cost = Math.round(val * Math.pow(1 + titleLevel * 0.3, 1.3) * 100) / 100;
return Number.isFinite(cost) ? cost : 0;
}
const cases = [
{ giftValue: 100, title: 3, lowest: 1 },
{ giftValue: '200', title: '2', lowest: '1' },
{ giftValue: null, title: null, lowest: null },
{ giftValue: undefined, title: undefined, lowest: undefined },
{ giftValue: 'abc', title: 5, lowest: 1 }
];
for (const c of cases) {
console.log(`in=${JSON.stringify(c)} -> cost=${getGiftCostLocal(c.giftValue, c.title, c.lowest)}`);
}

View File

@@ -1,38 +0,0 @@
import { fileURLToPath } from 'url';
import path from 'path';
import { readFileSync } from 'fs';
// Kleine Testhilfe: extrahiere getGiftCost aus service-file via eval (schneller Smoke-test ohne DB)
const svcPath = path.resolve(process.cwd(), 'services', 'falukantService.js');
const src = readFileSync(svcPath, 'utf8');
// Extrahiere die getGiftCost-Funktion via Regex (vereinfachte Annahme)
const re = /async getGiftCost\([\s\S]*?\n\s*}\n/;
const match = src.match(re);
if (!match) {
console.error('getGiftCost function not found');
process.exit(2);
}
const funcSrc = match[0];
// Wrappe in Async-Function und erzeuge getGiftCost im lokalen Scope
const wrapper = `(async () => { ${funcSrc}; return getGiftCost; })()`;
// eslint-disable-next-line no-eval
const getGiftCostPromise = eval(wrapper);
let getGiftCost;
getGiftCostPromise.then(f => { getGiftCost = f; runTests(); }).catch(e => { console.error('eval failed', e); process.exit(2); });
function runTests() {
const cases = [
{ value: 100, title: 3, lowest: 1 },
{ value: '200', title: '2', lowest: '1' },
{ value: null, title: null, lowest: null },
{ value: 'abc', title: 5, lowest: 1 }
];
for (const c of cases) {
getGiftCost(c.value, c.title, c.lowest).then(out => {
console.log(`in=${JSON.stringify(c)} -> cost=${out}`);
}).catch(err => console.error('error calling getGiftCost', err));
}
}
// Ende Patch

View File

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

View File

@@ -9,7 +9,6 @@ import PromotionalGiftMood from "../../models/falukant/predefine/promotional_gif
import { sequelize } from '../sequelize.js'; import { sequelize } from '../sequelize.js';
import HouseType from '../../models/falukant/type/house.js'; import HouseType from '../../models/falukant/type/house.js';
import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js"; import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js";
import TitleBenefit from "../../models/falukant/type/title_benefit.js";
import PartyType from "../../models/falukant/type/party.js"; import PartyType from "../../models/falukant/type/party.js";
import MusicType from "../../models/falukant/type/music.js"; import MusicType from "../../models/falukant/type/music.js";
import BanquetteType from "../../models/falukant/type/banquette.js"; import BanquetteType from "../../models/falukant/type/banquette.js";
@@ -17,8 +16,6 @@ import ReputationActionType from "../../models/falukant/type/reputation_action.j
import VehicleType from "../../models/falukant/type/vehicle.js"; import VehicleType from "../../models/falukant/type/vehicle.js";
import LearnRecipient from "../../models/falukant/type/learn_recipient.js"; import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js"; import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
import ChurchOfficeType from "../../models/falukant/type/church_office_type.js";
import ChurchOfficeRequirement from "../../models/falukant/predefine/church_office_requirement.js";
import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js"; import PoliticalOfficeBenefitType from "../../models/falukant/type/political_office_benefit_type.js";
import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js"; import PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
import UndergroundType from "../../models/falukant/type/underground.js"; import UndergroundType from "../../models/falukant/type/underground.js";
@@ -40,7 +37,6 @@ export const initializeFalukantTypes = async () => {
// Adelstitel VOR Haustypen initialisieren // Adelstitel VOR Haustypen initialisieren
await initializeFalukantTitlesOfNobility(); await initializeFalukantTitlesOfNobility();
await initializeTitleBenefits();
await initializeFalukantHouseTypes(); await initializeFalukantHouseTypes();
await initializeFalukantPartyTypes(); await initializeFalukantPartyTypes();
@@ -51,8 +47,6 @@ export const initializeFalukantTypes = async () => {
await initializePoliticalOfficeBenefitTypes(); await initializePoliticalOfficeBenefitTypes();
await initializePoliticalOfficeTypes(); await initializePoliticalOfficeTypes();
await initializePoliticalOfficePrerequisites(); await initializePoliticalOfficePrerequisites();
await initializeChurchOfficeTypes();
await initializeChurchOfficePrerequisites();
await initializeUndergroundTypes(); await initializeUndergroundTypes();
await initializeVehicleTypes(); await initializeVehicleTypes();
await initializeFalukantWeatherTypes(); await initializeFalukantWeatherTypes();
@@ -1030,136 +1024,6 @@ export const initializePoliticalOfficePrerequisites = async () => {
console.log(`[Falukant] OfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`); console.log(`[Falukant] OfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
}; };
// — Church Offices —
const churchOffices = [
{ tr: "lay-preacher", seatsPerRegion: 3, regionType: "city", hierarchyLevel: 0 },
{ tr: "village-priest", seatsPerRegion: 1, regionType: "city", hierarchyLevel: 1 },
{ tr: "parish-priest", seatsPerRegion: 1, regionType: "city", hierarchyLevel: 2 },
{ tr: "dean", seatsPerRegion: 1, regionType: "county", hierarchyLevel: 3 },
{ tr: "archdeacon", seatsPerRegion: 1, regionType: "shire", hierarchyLevel: 4 },
{ tr: "bishop", seatsPerRegion: 1, regionType: "markgravate", hierarchyLevel: 5 },
{ tr: "archbishop", seatsPerRegion: 1, regionType: "duchy", hierarchyLevel: 6 },
{ tr: "cardinal", seatsPerRegion: 3, regionType: "country", hierarchyLevel: 7 },
{ tr: "pope", seatsPerRegion: 1, regionType: "country", hierarchyLevel: 8 }
];
const churchOfficePrerequisites = [
{
officeTr: "lay-preacher",
prerequisite: {
prerequisiteOfficeTypeId: null // Einstiegsposition, keine Voraussetzung
}
},
{
officeTr: "village-priest",
prerequisite: {
prerequisiteOfficeTypeId: "lay-preacher"
}
},
{
officeTr: "parish-priest",
prerequisite: {
prerequisiteOfficeTypeId: "village-priest"
}
},
{
officeTr: "dean",
prerequisite: {
prerequisiteOfficeTypeId: "parish-priest"
}
},
{
officeTr: "archdeacon",
prerequisite: {
prerequisiteOfficeTypeId: "dean"
}
},
{
officeTr: "bishop",
prerequisite: {
prerequisiteOfficeTypeId: "archdeacon"
}
},
{
officeTr: "archbishop",
prerequisite: {
prerequisiteOfficeTypeId: "bishop"
}
},
{
officeTr: "cardinal",
prerequisite: {
prerequisiteOfficeTypeId: "archbishop"
}
},
{
officeTr: "pope",
prerequisite: {
prerequisiteOfficeTypeId: "cardinal"
}
}
];
export const initializeChurchOfficeTypes = async () => {
for (const co of churchOffices) {
await ChurchOfficeType.findOrCreate({
where: { name: co.tr },
defaults: {
seatsPerRegion: co.seatsPerRegion,
regionType: co.regionType,
hierarchyLevel: co.hierarchyLevel
}
});
}
console.log(`[Falukant] ChurchOfficeTypes initialized`);
};
export const initializeChurchOfficePrerequisites = async () => {
let created = 0;
let existing = 0;
let skipped = 0;
for (const prereq of churchOfficePrerequisites) {
const office = await ChurchOfficeType.findOne({ where: { name: prereq.officeTr } });
if (!office) { skipped++; continue; }
let prerequisiteOfficeTypeId = null;
if (prereq.prerequisite.prerequisiteOfficeTypeId) {
const prerequisiteOffice = await ChurchOfficeType.findOne({
where: { name: prereq.prerequisite.prerequisiteOfficeTypeId }
});
if (prerequisiteOffice) {
prerequisiteOfficeTypeId = prerequisiteOffice.id;
}
}
try {
const [record, wasCreated] = await ChurchOfficeRequirement.findOrCreate({
where: { officeTypeId: office.id },
defaults: {
officeTypeId: office.id,
prerequisiteOfficeTypeId: prerequisiteOfficeTypeId
}
});
if (wasCreated) {
created++;
} else {
// Aktualisiere, falls sich die Voraussetzung geändert hat
if (record.prerequisiteOfficeTypeId !== prerequisiteOfficeTypeId) {
await record.update({ prerequisiteOfficeTypeId: prerequisiteOfficeTypeId });
created++; // Zähle als Update
} else {
existing++;
}
}
} catch (e) {
if (falukantDebug) console.error('[Falukant] ChurchOfficePrereq Fehler', { officeId: office?.id, error: e.message });
throw e;
}
}
console.log(`[Falukant] ChurchOfficePrereq neu=${created} exist=${existing}${skipped?` skip=${skipped}`:''}`);
};
export const initializeUndergroundTypes = async () => { export const initializeUndergroundTypes = async () => {
for (const underground of undergroundTypes) { for (const underground of undergroundTypes) {
await UndergroundType.findOrCreate({ await UndergroundType.findOrCreate({
@@ -1206,65 +1070,6 @@ export const initializeFalukantTitlesOfNobility = async () => {
} }
}; };
/** Standesvorteile: tax_share, tax_exempt, office_eligibility, free_party_type, reputation_bonus */
async function initializeTitleBenefits() {
const titles = await TitleOfNobility.findAll({ attributes: ['id', 'labelTr', 'level'] });
const byLabel = new Map(titles.map(t => [t.labelTr, t]));
const benefits = [];
// tax_share: oberster Stand einer Region bekommt Steuer Titel mit hohem level (z.B. ab count)
const taxShareTitles = ['count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'];
for (const label of taxShareTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'tax_share', parameters: {} });
}
// tax_exempt: z.B. noncivil, oder hohe Titel
const taxExemptTitles = ['noncivil', 'king', 'prince-regent'];
for (const label of taxExemptTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'tax_exempt', parameters: {} });
}
// office_eligibility: pro Titel eine Zeile mit allen erlaubten Ämtern (officeTypeNames)
const officeEligibility = [
{ label: 'assessor', titles: ['civil', 'sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
{ label: 'council', titles: ['sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
{ label: 'taxman', titles: ['townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
{ label: 'chancellor', titles: ['king'] }
];
const titleToOffices = new Map();
for (const { label: officeName, titles: allowedTitles } of officeEligibility) {
for (const label of allowedTitles) {
const t = byLabel.get(label);
if (t) {
const list = titleToOffices.get(t.id) || [];
if (!list.includes(officeName)) list.push(officeName);
titleToOffices.set(t.id, list);
}
}
}
for (const [titleId, officeTypeNames] of titleToOffices) {
benefits.push({ titleId, benefitType: 'office_eligibility', parameters: { officeTypeNames } });
}
// free_party_type: z.B. "wedding" für civil+ (partyTypeIds oder labelTr)
const freePartyTitles = ['sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'];
for (const label of freePartyTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'free_party_type', parameters: { partyTypeLabelTrs: ['wedding'] } });
}
// reputation_bonus: zufällig 515 % für ausgewählte Stände (hier: knight, count, duke)
const reputationBonusTitles = ['knight', 'count', 'duke'];
for (const label of reputationBonusTitles) {
const t = byLabel.get(label);
if (t) benefits.push({ titleId: t.id, benefitType: 'reputation_bonus', parameters: { minPercent: 5, maxPercent: 15 } });
}
for (const b of benefits) {
await TitleBenefit.findOrCreate({
where: { titleId: b.titleId, benefitType: b.benefitType },
defaults: { parameters: b.parameters }
});
}
console.log(`[Falukant] Standesvorteile (title_benefit) initialisiert: ${benefits.length} Einträge`);
}
const weatherTypes = [ const weatherTypes = [
{ tr: "sunny" }, { tr: "sunny" },
{ tr: "cloudy" }, { tr: "cloudy" },

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