Compare commits
5 Commits
Redesign
...
falukant-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d74f7b852b | ||
|
|
92d6b15c3f | ||
|
|
91f59062f5 | ||
|
|
1674086c73 | ||
|
|
5ddb099f5a |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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
|
|
||||||
|
|||||||
156
CHURCH_MODELS.md
156
CHURCH_MODELS.md
@@ -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'
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
119
CMakeLists.txt
119
CMakeLists.txt
@@ -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)
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
168
SSL-SETUP.md
168
SSL-SETUP.md
@@ -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`
|
|
||||||
@@ -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();
|
|
||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 1236
|
"port": 1235
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,52 +12,26 @@ 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
|
|
||||||
try {
|
|
||||||
fs.accessSync(productionEnvPath, fs.constants.R_OK);
|
|
||||||
envPath = productionEnvPath;
|
envPath = productionEnvPath;
|
||||||
usingProduction = true;
|
console.log('[env] Lade Produktions-.env:', productionEnvPath);
|
||||||
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
|
||||||
console.log('[env] Redis-Konfiguration:');
|
console.log('[env] Redis-Konfiguration:');
|
||||||
|
|||||||
@@ -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' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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,14 +252,8 @@ 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
|
|
||||||
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' });
|
res.status(status).json({ error: error.message || 'Internal error' });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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 } = {}) {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -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} ...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
@@ -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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
4322
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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());
|
||||||
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
|
io.emit('newMessage', message); // Broadcast an alle Clients
|
||||||
} catch (err) {
|
}, { noAck: true });
|
||||||
console.warn('[webSocketService] sendToQueue fehlgeschlagen, nutze In-Memory-Fallback:', err.message);
|
|
||||||
channel = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channel) {
|
// Empfangt eine Nachricht vom WebSocket-Client und sendet sie an die RabbitMQ-Warteschlange
|
||||||
pendingMessages.push(message);
|
socket.on('newMessage', (message) => {
|
||||||
if (pendingMessages.length > MAX_PENDING_MESSAGES) {
|
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
|
||||||
pendingMessages = pendingMessages.slice(-MAX_PENDING_MESSAGES);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
console.log('Client disconnected');
|
console.log('Client disconnected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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".';
|
|
||||||
@@ -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
|
|
||||||
-- ============================================
|
|
||||||
@@ -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.
|
|
||||||
@@ -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!
|
|
||||||
-- ============================================
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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)}`);
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 5–15 % 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
Reference in New Issue
Block a user