Compare commits
295 Commits
falukant-3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aecd9a8245 | ||
|
|
4f3439e835 | ||
|
|
a5bec5baf7 | ||
|
|
8d23453371 | ||
|
|
2184c4a7e1 | ||
|
|
ba5e36fa55 | ||
|
|
70d1d48fbc | ||
|
|
d23026121e | ||
|
|
057b038fac | ||
|
|
0697f3d363 | ||
|
|
400d44289c | ||
|
|
bbc3354f16 | ||
|
|
d038d72cde | ||
|
|
16e54d20d0 | ||
|
|
14775eb556 | ||
|
|
ce34bae16a | ||
|
|
640cdcf671 | ||
|
|
f15924c0be | ||
|
|
0d32c5b4b3 | ||
|
|
101050ce58 | ||
|
|
b16249e7c2 | ||
|
|
8b63344bc2 | ||
|
|
b648175205 | ||
|
|
4bf1bc35ae | ||
|
|
067273d428 | ||
|
|
7ed284d74b | ||
|
|
f65d3385ec | ||
|
|
7635355e94 | ||
|
|
ec75c7ecdb | ||
|
|
786420d1d2 | ||
|
|
cff0ce1e1a | ||
|
|
8355f985cd | ||
|
|
25af538c88 | ||
|
|
d1503cd813 | ||
|
|
7d2a33b3ec | ||
|
|
752686e3e1 | ||
|
|
3870f34ef8 | ||
|
|
ae71a066c7 | ||
|
|
b52327db2e | ||
|
|
d5c089e07e | ||
|
|
0f78c624b1 | ||
|
|
e1632c41c2 | ||
|
|
323b051355 | ||
|
|
3999b17e88 | ||
|
|
8fd15614af | ||
|
|
ddefc2737b | ||
|
|
05868d8a09 | ||
|
|
b3afb988a3 | ||
|
|
3b8e0573f2 | ||
|
|
4779a6e4af | ||
|
|
39ac149430 | ||
|
|
8ec7db031b | ||
|
|
25b5b91a19 | ||
|
|
e8c6f6ffb9 | ||
|
|
62d8cd7b05 | ||
|
|
c09159d6ce | ||
|
|
8d2db95540 | ||
|
|
9519846489 | ||
|
|
f7a977df33 | ||
|
|
f1717920b6 | ||
|
|
c5ab17ad99 | ||
|
|
1839c3c57b | ||
|
|
ba63b3504f | ||
|
|
032e336b65 | ||
|
|
474e46837a | ||
|
|
e7052636ba | ||
|
|
cb2631061e | ||
|
|
d1ddfe7d31 | ||
|
|
59cad22183 | ||
|
|
57d64a7ef8 | ||
|
|
ae096eb4c3 | ||
|
|
789861999c | ||
|
|
72f4bd066d | ||
|
|
b3db65d1b8 | ||
|
|
506a9cd9c0 | ||
|
|
1ead06fd4f | ||
|
|
eecd947377 | ||
|
|
5351e3ea57 | ||
|
|
3bdb77888f | ||
|
|
c570fd6ae3 | ||
|
|
be3ed4af5d | ||
|
|
4cce044128 | ||
|
|
59875cf900 | ||
|
|
37129055e6 | ||
|
|
934e80c2ab | ||
|
|
8e20fbd24d | ||
|
|
f102069f5a | ||
|
|
afc36161ed | ||
|
|
a8b76bc21a | ||
|
|
8550bd31d9 | ||
|
|
8837494a06 | ||
|
|
0c407b81b7 | ||
|
|
71b4a02592 | ||
|
|
83e5767812 | ||
|
|
c544c2c7f9 | ||
|
|
818c8fbdf9 | ||
|
|
a6326f149d | ||
|
|
01679697b4 | ||
|
|
d4fb2a8ccc | ||
|
|
08b6437a1e | ||
|
|
baffd9d05c | ||
|
|
cbff7c130c | ||
|
|
16f3d1a320 | ||
|
|
955ea1a9ed | ||
|
|
ca614f6cc2 | ||
|
|
71748f6aa0 | ||
|
|
80b639b511 | ||
|
|
bba68da488 | ||
|
|
29c2b53f53 | ||
|
|
c3cc248a39 | ||
|
|
fb821dbf21 | ||
|
|
079250fcd7 | ||
|
|
120cb5fadd | ||
|
|
d3a554108f | ||
|
|
6471158847 | ||
|
|
1c442eb195 | ||
|
|
13f5660fee | ||
|
|
9333a8318c | ||
|
|
c1cda5fa62 | ||
|
|
88967ba9d3 | ||
|
|
92d792246c | ||
|
|
586aaec506 | ||
|
|
10690b5a6e | ||
|
|
bceef9777a | ||
|
|
4f786cdcc3 | ||
|
|
8e226615eb | ||
|
|
82734e8383 | ||
|
|
69a83c584b | ||
|
|
a8fdcd179e | ||
|
|
ace976965d | ||
|
|
7303d1ea0b | ||
|
|
4379b0b955 | ||
|
|
09af7af228 | ||
|
|
dc08da211f | ||
|
|
30e1df0dd8 | ||
|
|
95a4c977c1 | ||
|
|
6ce081196c | ||
|
|
3d5342b314 | ||
|
|
78d43e6859 | ||
|
|
41106ae306 | ||
|
|
33aa2ddd45 | ||
|
|
2be5505c55 | ||
|
|
8c0f07cc51 | ||
|
|
3018b1f2e1 | ||
|
|
a21a2314d7 | ||
|
|
a76aae3d12 | ||
|
|
7765067d1b | ||
|
|
eddbe5fa3f | ||
|
|
c907d2773d | ||
|
|
5f71e56bf9 | ||
|
|
adcbd1a95a | ||
|
|
175a61c81c | ||
|
|
4d97f24531 | ||
|
|
8d32d704b5 | ||
|
|
e5d4a5f95f | ||
|
|
d4a0f78cd0 | ||
|
|
7cd946181e | ||
|
|
cf97a3ba5e | ||
|
|
963e0c906c | ||
|
|
089743ac23 | ||
|
|
69ef120677 | ||
|
|
fe2e6a53e9 | ||
|
|
cf1b5e7f71 | ||
|
|
202002358a | ||
|
|
14eb28d37f | ||
|
|
81dbbdd6f5 | ||
|
|
9e6787fb3f | ||
|
|
2eee7bb0c1 | ||
|
|
7f57ecc35e | ||
|
|
21f6130666 | ||
|
|
594b3dac4a | ||
|
|
ef2b279df6 | ||
|
|
2ffd7a6151 | ||
|
|
045d32c245 | ||
|
|
053588ae74 | ||
|
|
749a2d6f59 | ||
|
|
95ba8f0b33 | ||
|
|
dacf6cb7f8 | ||
|
|
656c3b3d09 | ||
|
|
44ce6636c0 | ||
|
|
1413630f11 | ||
|
|
8f55f63f77 | ||
|
|
0331ffeb93 | ||
|
|
196b74bebb | ||
|
|
305e137a1a | ||
|
|
4e5ddc8027 | ||
|
|
4bb75de3f0 | ||
|
|
0572a0eb50 | ||
|
|
c13cb40c7b | ||
|
|
33787ba796 | ||
|
|
64f4468664 | ||
|
|
408b65be30 | ||
|
|
891420cb09 | ||
|
|
a657c59b2c | ||
|
|
89ec084106 | ||
|
|
a7a0daaf82 | ||
|
|
df5c2a3141 | ||
|
|
f902f5298c | ||
|
|
ddd038761b | ||
|
|
09e53244d9 | ||
|
|
714e144329 | ||
|
|
e1b3dfb00a | ||
|
|
b6a4607e60 | ||
|
|
9553cc811a | ||
|
|
59c05b3628 | ||
|
|
d3629a8a09 | ||
|
|
a17e8537fb | ||
|
|
a7f23c5885 | ||
|
|
b706191a0e | ||
|
|
ba469ef900 | ||
|
|
e852346b94 | ||
|
|
02d24eccd8 | ||
|
|
d1359ccc36 | ||
|
|
52c7f1c7ba | ||
|
|
7a2749c405 | ||
|
|
d71df901ed | ||
|
|
1af4b6c2e4 | ||
|
|
2595cb8565 | ||
|
|
45d549aa4e | ||
|
|
7f65f5e40e | ||
|
|
5ce1cc4e6a | ||
|
|
3a6d60e9a8 | ||
|
|
d5a09f359d | ||
|
|
127e95ca1c | ||
|
|
bb81126cd8 | ||
|
|
2d3d120f81 | ||
|
|
0c36c4a4e5 | ||
|
|
88f6686809 | ||
|
|
9c7b682a36 | ||
|
|
dafdbf0a84 | ||
|
|
5ac8e9b484 | ||
|
|
753c5929e1 | ||
|
|
e3f46d775a | ||
|
|
0eb3a78332 | ||
|
|
3ac9f25284 | ||
|
|
b3c9c8f37c | ||
|
|
32bc126def | ||
|
|
00a5f47cae | ||
|
|
6a1260687b | ||
|
|
7591787583 | ||
|
|
bd961a03d4 | ||
|
|
8fe816dddc | ||
|
|
e7a8dc86eb | ||
|
|
c9dc891481 | ||
|
|
89c3873db7 | ||
|
|
60352d7932 | ||
|
|
664f2af346 | ||
|
|
8212e906a3 | ||
|
|
92e17a9f43 | ||
|
|
d3727ad2f7 | ||
|
|
391e5d9992 | ||
|
|
a4bd585730 | ||
|
|
c694769f4c | ||
|
|
8b9ff9793c | ||
|
|
8ba4566d23 | ||
|
|
91420b9973 | ||
|
|
8d3e0423e7 | ||
|
|
4bafc3a61c | ||
|
|
1f43df6d41 | ||
|
|
c2a54e29f8 | ||
|
|
b1f9073f4d | ||
|
|
1b38e2412c | ||
|
|
4b9311713a | ||
|
|
77520ee46a | ||
|
|
23c07a3570 | ||
|
|
1451225978 | ||
|
|
51fd9fcd13 | ||
|
|
1fe77c0905 | ||
|
|
cd739fb52e | ||
|
|
9e845843d8 | ||
|
|
0cc280ed55 | ||
|
|
b3707d21b2 | ||
|
|
fbebd6c1c1 | ||
|
|
d7c2bda461 | ||
|
|
2bf949513b | ||
|
|
84619fb656 | ||
|
|
b600f16ecd | ||
|
|
9273066f61 | ||
|
|
7d59dbcf84 | ||
|
|
015d1ae95b | ||
|
|
e2cd6e0e5e | ||
|
|
ec113058d0 | ||
|
|
d2ac2bfdd8 | ||
|
|
d75fe18e6a | ||
|
|
479f222b54 | ||
|
|
013c536b47 | ||
|
|
3b983a0db5 | ||
|
|
5f9559ac8d | ||
|
|
f487e6d765 | ||
|
|
5e26422e9c | ||
|
|
64baebfaaa | ||
|
|
521dec24b2 | ||
|
|
36f0bd8eb9 | ||
|
|
d0a2b122b2 | ||
|
|
c80cc8ec86 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
.depbe.sh
|
||||
node_modules
|
||||
node_modules/*
|
||||
**/package-lock.json
|
||||
backend/.env
|
||||
backend/images
|
||||
backend/images/*
|
||||
@@ -17,3 +18,9 @@ frontend/dist
|
||||
frontend/dist/*
|
||||
frontedtree.txt
|
||||
backend/dist/
|
||||
backend/data/model-cache
|
||||
build
|
||||
build/*
|
||||
.vscode
|
||||
.vscode/*
|
||||
.clang-format
|
||||
|
||||
156
CHURCH_MODELS.md
Normal file
156
CHURCH_MODELS.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Church Models - Übersicht für Daemon-Entwicklung
|
||||
|
||||
## 1. ChurchOfficeType (falukant_type.church_office_type)
|
||||
|
||||
**Schema:** `falukant_type`
|
||||
**Tabelle:** `church_office_type`
|
||||
**Zweck:** Definiert die verschiedenen Kirchenämter-Typen
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: INTEGER (PK, auto-increment)
|
||||
name: STRING (z.B. "pope", "cardinal", "lay-preacher")
|
||||
seatsPerRegion: INTEGER (Anzahl verfügbarer Plätze pro Region)
|
||||
regionType: STRING (z.B. "country", "duchy", "city")
|
||||
hierarchyLevel: INTEGER (0-8, höhere Zahl = höhere Position)
|
||||
}
|
||||
```
|
||||
|
||||
**Beziehungen:**
|
||||
- `hasMany` ChurchOffice (als `offices`)
|
||||
- `hasMany` ChurchApplication (als `applications`)
|
||||
- `hasMany` ChurchOfficeRequirement (als `requirements`)
|
||||
|
||||
---
|
||||
|
||||
## 2. ChurchOfficeRequirement (falukant_predefine.church_office_requirement)
|
||||
|
||||
**Schema:** `falukant_predefine`
|
||||
**Tabelle:** `church_office_requirement`
|
||||
**Zweck:** Definiert Voraussetzungen für Kirchenämter
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: INTEGER (PK, auto-increment)
|
||||
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
||||
prerequisiteOfficeTypeId: INTEGER (FK -> ChurchOfficeType.id, nullable)
|
||||
minTitleLevel: INTEGER (nullable, optional)
|
||||
}
|
||||
```
|
||||
|
||||
**Beziehungen:**
|
||||
- `belongsTo` ChurchOfficeType (als `officeType`)
|
||||
- `belongsTo` ChurchOfficeType (als `prerequisiteOfficeType`)
|
||||
|
||||
---
|
||||
|
||||
## 3. ChurchOffice (falukant_data.church_office)
|
||||
|
||||
**Schema:** `falukant_data`
|
||||
**Tabelle:** `church_office`
|
||||
**Zweck:** Speichert tatsächlich besetzte Kirchenämter
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: INTEGER (PK, auto-increment)
|
||||
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
||||
characterId: INTEGER (FK -> FalukantCharacter.id)
|
||||
regionId: INTEGER (FK -> RegionData.id)
|
||||
supervisorId: INTEGER (FK -> FalukantCharacter.id, nullable)
|
||||
createdAt: DATE
|
||||
updatedAt: DATE
|
||||
}
|
||||
```
|
||||
|
||||
**Beziehungen:**
|
||||
- `belongsTo` ChurchOfficeType (als `type`)
|
||||
- `belongsTo` FalukantCharacter (als `holder`)
|
||||
- `belongsTo` FalukantCharacter (als `supervisor`)
|
||||
- `belongsTo` RegionData (als `region`)
|
||||
|
||||
---
|
||||
|
||||
## 4. ChurchApplication (falukant_data.church_application)
|
||||
|
||||
**Schema:** `falukant_data`
|
||||
**Tabelle:** `church_application`
|
||||
**Zweck:** Speichert Bewerbungen für Kirchenämter
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: INTEGER (PK, auto-increment)
|
||||
officeTypeId: INTEGER (FK -> ChurchOfficeType.id)
|
||||
characterId: INTEGER (FK -> FalukantCharacter.id)
|
||||
regionId: INTEGER (FK -> RegionData.id)
|
||||
supervisorId: INTEGER (FK -> FalukantCharacter.id)
|
||||
status: ENUM('pending', 'approved', 'rejected')
|
||||
decisionDate: DATE (nullable)
|
||||
createdAt: DATE
|
||||
updatedAt: DATE
|
||||
}
|
||||
```
|
||||
|
||||
**Beziehungen:**
|
||||
- `belongsTo` ChurchOfficeType (als `officeType`)
|
||||
- `belongsTo` FalukantCharacter (als `applicant`)
|
||||
- `belongsTo` FalukantCharacter (als `supervisor`)
|
||||
- `belongsTo` RegionData (als `region`)
|
||||
|
||||
---
|
||||
|
||||
## Zusätzlich benötigte Models (für Daemon)
|
||||
|
||||
### RegionData (falukant_data.region)
|
||||
- Wird für `regionId` in ChurchOffice und ChurchApplication benötigt
|
||||
- Enthält `regionType` (country, duchy, markgravate, shire, county, city)
|
||||
- Enthält `parentId` für Hierarchie
|
||||
|
||||
### FalukantCharacter (falukant_data.character)
|
||||
- Wird für `characterId` (Inhaber/Bewerber) benötigt
|
||||
- Wird für `supervisorId` benötigt
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Queries für Daemon
|
||||
|
||||
### Verfügbare Positionen finden
|
||||
```sql
|
||||
SELECT cot.*, COUNT(co.id) as occupied_seats
|
||||
FROM falukant_type.church_office_type cot
|
||||
LEFT JOIN falukant_data.church_office co
|
||||
ON cot.id = co.office_type_id
|
||||
AND co.region_id = ?
|
||||
WHERE cot.region_type = ?
|
||||
GROUP BY cot.id
|
||||
HAVING COUNT(co.id) < cot.seats_per_region
|
||||
```
|
||||
|
||||
### Supervisor finden
|
||||
```sql
|
||||
SELECT co.*
|
||||
FROM falukant_data.church_office co
|
||||
JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id
|
||||
WHERE co.region_id = ?
|
||||
AND cot.hierarchy_level > (
|
||||
SELECT hierarchy_level
|
||||
FROM falukant_type.church_office_type
|
||||
WHERE id = ?
|
||||
)
|
||||
ORDER BY cot.hierarchy_level ASC
|
||||
LIMIT 1
|
||||
```
|
||||
|
||||
### Voraussetzungen prüfen
|
||||
```sql
|
||||
SELECT cor.*
|
||||
FROM falukant_predefine.church_office_requirement cor
|
||||
WHERE cor.office_type_id = ?
|
||||
```
|
||||
|
||||
### Bewerbungen für Supervisor
|
||||
```sql
|
||||
SELECT ca.*
|
||||
FROM falukant_data.church_application ca
|
||||
WHERE ca.supervisor_id = ?
|
||||
AND ca.status = 'pending'
|
||||
```
|
||||
78
CHURCH_OFFICES.md
Normal file
78
CHURCH_OFFICES.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Kirchenämter - Hierarchie und Verfügbarkeit
|
||||
|
||||
## Regionstypen
|
||||
- **country** (Land): Falukant
|
||||
- **duchy** (Herzogtum): Hessen
|
||||
- **markgravate** (Markgrafschaft): Groß-Benbach
|
||||
- **shire** (Grafschaft): Siebenbachen
|
||||
- **county** (Kreis): Bad Homburg, Maintal
|
||||
- **city** (Stadt): Frankfurt, Oberursel, Offenbach, Königstein
|
||||
|
||||
## Kirchenämter (von höchstem zu niedrigstem Rang)
|
||||
|
||||
| Amt | Translation Key | Hierarchie-Level | Regionstyp | Plätze pro Region | Beschreibung |
|
||||
|-----|----------------|-------------------|------------|-------------------|--------------|
|
||||
| **Papst** | `pope` | 8 | country | 1 | Höchstes Amt, nur einer im ganzen Land |
|
||||
| **Kardinal** | `cardinal` | 7 | country | 3 | Höchste Kardinäle, mehrere pro Land möglich |
|
||||
| **Erzbischof** | `archbishop` | 6 | duchy | 1 | Pro Herzogtum ein Erzbischof |
|
||||
| **Bischof** | `bishop` | 5 | markgravate | 1 | Pro Markgrafschaft ein Bischof |
|
||||
| **Erzdiakon** | `archdeacon` | 4 | shire | 1 | Pro Grafschaft ein Erzdiakon |
|
||||
| **Dekan** | `dean` | 3 | county | 1 | Pro Kreis ein Dekan |
|
||||
| **Pfarrer** | `parish-priest` | 2 | city | 1 | Pro Stadt ein Pfarrer |
|
||||
| **Dorfgeistlicher** | `village-priest` | 1 | city | 1 | Pro Stadt ein Dorfgeistlicher (Einstiegsposition) |
|
||||
| **Laienprediger** | `lay-preacher` | 0 | city | 3 | Pro Stadt mehrere Laienprediger (niedrigste Position) |
|
||||
|
||||
## Verfügbare Positionen pro Regionstyp
|
||||
|
||||
### country (Land: Falukant)
|
||||
- **Papst**: 1 Platz
|
||||
- **Kardinal**: 3 Plätze
|
||||
- **Gesamt**: 4 Plätze
|
||||
|
||||
### duchy (Herzogtum: Hessen)
|
||||
- **Erzbischof**: 1 Platz
|
||||
- **Gesamt**: 1 Platz
|
||||
|
||||
### markgravate (Markgrafschaft: Groß-Benbach)
|
||||
- **Bischof**: 1 Platz
|
||||
- **Gesamt**: 1 Platz
|
||||
|
||||
### shire (Grafschaft: Siebenbachen)
|
||||
- **Erzdiakon**: 1 Platz
|
||||
- **Gesamt**: 1 Platz
|
||||
|
||||
### county (Kreis: Bad Homburg, Maintal)
|
||||
- **Dekan**: 1 Platz pro Kreis
|
||||
- **Gesamt**: 1 Platz pro Kreis
|
||||
|
||||
### city (Stadt: Frankfurt, Oberursel, Offenbach, Königstein)
|
||||
- **Pfarrer**: 1 Platz pro Stadt
|
||||
- **Dorfgeistlicher**: 1 Platz pro Stadt
|
||||
- **Laienprediger**: 3 Plätze pro Stadt
|
||||
- **Gesamt**: 5 Plätze pro Stadt
|
||||
|
||||
## Hierarchie und Beförderungsweg
|
||||
|
||||
1. **Laienprediger** (lay-preacher) - Einstiegsposition, keine Voraussetzung
|
||||
2. **Dorfgeistlicher** (village-priest) - Voraussetzung: Laienprediger
|
||||
3. **Pfarrer** (parish-priest) - Voraussetzung: Dorfgeistlicher
|
||||
4. **Dekan** (dean) - Voraussetzung: Pfarrer
|
||||
5. **Erzdiakon** (archdeacon) - Voraussetzung: Dekan
|
||||
6. **Bischof** (bishop) - Voraussetzung: Erzdiakon
|
||||
7. **Erzbischof** (archbishop) - Voraussetzung: Bischof
|
||||
8. **Kardinal** (cardinal) - Voraussetzung: Erzbischof
|
||||
9. **Papst** (pope) - Voraussetzung: Kardinal
|
||||
|
||||
## Gesamtübersicht verfügbarer Positionen
|
||||
|
||||
- **Papst**: 1 Position (Land)
|
||||
- **Kardinal**: 3 Positionen (Land)
|
||||
- **Erzbischof**: 1 Position (Herzogtum)
|
||||
- **Bischof**: 1 Position (Markgrafschaft)
|
||||
- **Erzdiakon**: 1 Position (Grafschaft)
|
||||
- **Dekan**: 2 Positionen (2 Kreise)
|
||||
- **Pfarrer**: 4 Positionen (4 Städte)
|
||||
- **Dorfgeistlicher**: 4 Positionen (4 Städte)
|
||||
- **Laienprediger**: 12 Positionen (4 Städte × 3)
|
||||
|
||||
**Gesamt**: 30 Positionen im System
|
||||
119
CMakeLists.txt
Normal file
119
CMakeLists.txt
Normal file
@@ -0,0 +1,119 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(YourPartDaemon VERSION 1.0 LANGUAGES CXX)
|
||||
|
||||
# C++ Standard and Compiler Settings
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
# Use best available GCC for C++23 support (OpenSUSE Tumbleweed)
|
||||
# Try GCC 15 first (best C++23 support), then GCC 13, then system default
|
||||
find_program(GCC15_CC gcc-15)
|
||||
find_program(GCC15_CXX g++-15)
|
||||
find_program(GCC13_CC gcc-13)
|
||||
find_program(GCC13_CXX g++-13)
|
||||
|
||||
if(GCC15_CC AND GCC15_CXX)
|
||||
set(CMAKE_C_COMPILER ${GCC15_CC})
|
||||
set(CMAKE_CXX_COMPILER ${GCC15_CXX})
|
||||
message(STATUS "Using GCC 15 for best C++23 support")
|
||||
elseif(GCC13_CC AND GCC13_CXX)
|
||||
set(CMAKE_C_COMPILER ${GCC13_CC})
|
||||
set(CMAKE_CXX_COMPILER ${GCC13_CXX})
|
||||
message(STATUS "Using GCC 13 for C++23 support")
|
||||
else()
|
||||
message(STATUS "Using system default compiler")
|
||||
endif()
|
||||
# Optimize for GCC 13 with C++23
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto=auto -O3 -march=native -mtune=native")
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "-O1 -g -DDEBUG")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -march=native -mtune=native")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
|
||||
# Include /usr/local if needed
|
||||
list(APPEND CMAKE_PREFIX_PATH /usr/local)
|
||||
|
||||
# Find libwebsockets via pkg-config
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LWS REQUIRED libwebsockets)
|
||||
|
||||
# Find other dependencies
|
||||
find_package(PostgreSQL REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
|
||||
# PostgreSQL C++ libpqxx
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBPQXX REQUIRED libpqxx)
|
||||
|
||||
# Project sources and headers
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/config.cpp
|
||||
src/connection_pool.cpp
|
||||
src/database.cpp
|
||||
src/character_creation_worker.cpp
|
||||
src/produce_worker.cpp
|
||||
src/message_broker.cpp
|
||||
src/websocket_server.cpp
|
||||
src/stockagemanager.cpp
|
||||
src/director_worker.cpp
|
||||
src/valuerecalculationworker.cpp
|
||||
src/usercharacterworker.cpp
|
||||
src/houseworker.cpp
|
||||
src/politics_worker.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
src/config.h
|
||||
src/database.h
|
||||
src/connection_pool.h
|
||||
src/worker.h
|
||||
src/character_creation_worker.h
|
||||
src/produce_worker.h
|
||||
src/message_broker.h
|
||||
src/websocket_server.h
|
||||
src/stockagemanager.h
|
||||
src/director_worker.h
|
||||
src/valuerecalculationworker.h
|
||||
src/usercharacterworker.h
|
||||
src/houseworker.h
|
||||
src/politics_worker.h
|
||||
)
|
||||
|
||||
# Define executable target
|
||||
add_executable(yourpart-daemon ${SOURCES} ${HEADERS}
|
||||
src/utils.h src/utils.cpp
|
||||
src/underground_worker.h src/underground_worker.cpp)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(yourpart-daemon PRIVATE
|
||||
${PostgreSQL_INCLUDE_DIRS}
|
||||
${LIBPQXX_INCLUDE_DIRS}
|
||||
${LWS_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# Find systemd
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(SYSTEMD REQUIRED libsystemd)
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(yourpart-daemon PRIVATE
|
||||
${PostgreSQL_LIBRARIES}
|
||||
Threads::Threads
|
||||
z ssl crypto
|
||||
${LIBPQXX_LIBRARIES}
|
||||
${LWS_LIBRARIES}
|
||||
nlohmann_json::nlohmann_json
|
||||
${SYSTEMD_LIBRARIES}
|
||||
)
|
||||
|
||||
# Installation rules
|
||||
install(TARGETS yourpart-daemon DESTINATION /usr/local/bin)
|
||||
|
||||
# Installiere Template als Referenz ZUERST (wird vom install-Skript benötigt)
|
||||
install(FILES daemon.conf DESTINATION /etc/yourpart/ RENAME daemon.conf.example)
|
||||
|
||||
# Intelligente Konfigurationsdatei-Installation
|
||||
# Verwendet ein CMake-Skript, das nur fehlende Keys hinzufügt, ohne bestehende zu überschreiben
|
||||
# Das Skript liest das Template aus /etc/yourpart/daemon.conf.example oder dem Source-Verzeichnis
|
||||
install(SCRIPT cmake/install-config.cmake)
|
||||
414
CMakeLists.txt.user
Normal file
414
CMakeLists.txt.user
Normal file
@@ -0,0 +1,414 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE QtCreatorProject>
|
||||
<!-- Written by QtCreator 17.0.0, 2025-08-16T22:07:06. -->
|
||||
<qtcreator>
|
||||
<data>
|
||||
<variable>EnvironmentId</variable>
|
||||
<value type="QByteArray">{551ef6b3-a39b-43e2-9ee3-ad56e19ff4f4}</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.ActiveTarget</variable>
|
||||
<value type="qlonglong">0</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.EditorSettings</variable>
|
||||
<valuemap type="QVariantMap">
|
||||
<value type="bool" key="EditorConfiguration.AutoDetect">true</value>
|
||||
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
|
||||
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
|
||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
|
||||
<value type="QString" key="language">Cpp</value>
|
||||
<valuemap type="QVariantMap" key="value">
|
||||
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
|
||||
</valuemap>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
|
||||
<value type="QString" key="language">QmlJS</value>
|
||||
<valuemap type="QVariantMap" key="value">
|
||||
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
|
||||
</valuemap>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
|
||||
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
|
||||
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
|
||||
<value type="int" key="EditorConfiguration.IndentSize">4</value>
|
||||
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
|
||||
<value type="int" key="EditorConfiguration.LineEndingBehavior">0</value>
|
||||
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
|
||||
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
|
||||
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
|
||||
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
|
||||
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
|
||||
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
|
||||
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
|
||||
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
|
||||
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">2</value>
|
||||
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
|
||||
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
|
||||
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
|
||||
<value type="int" key="EditorConfiguration.TabSize">8</value>
|
||||
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
|
||||
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
|
||||
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
|
||||
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
|
||||
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
|
||||
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
|
||||
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
|
||||
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
|
||||
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
|
||||
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
|
||||
</valuemap>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.PluginSettings</variable>
|
||||
<valuemap type="QVariantMap">
|
||||
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
|
||||
<value type="bool" key="AutoTest.Framework.Boost">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.CTest">false</value>
|
||||
<value type="bool" key="AutoTest.Framework.Catch">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.GTest">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
|
||||
</valuemap>
|
||||
<value type="bool" key="AutoTest.ApplyFilter">false</value>
|
||||
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
|
||||
<valuelist type="QVariantList" key="AutoTest.PathFilters"/>
|
||||
<value type="int" key="AutoTest.RunAfterBuild">0</value>
|
||||
<value type="bool" key="AutoTest.UseGlobal">true</value>
|
||||
<valuemap type="QVariantMap" key="ClangTools">
|
||||
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
|
||||
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
|
||||
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
|
||||
<value type="int" key="ClangTools.ParallelJobs">8</value>
|
||||
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
|
||||
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
|
||||
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
|
||||
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
|
||||
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
|
||||
</valuemap>
|
||||
</valuemap>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.Target.0</variable>
|
||||
<valuemap type="QVariantMap">
|
||||
<value type="QString" key="DeviceType">Desktop</value>
|
||||
<value type="bool" key="HasPerBcDcs">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{78ff90a3-f672-45c2-ad08-343b0923896f}</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
|
||||
<value type="QString" key="CMake.Build.Type">Debug</value>
|
||||
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
||||
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
||||
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
|
||||
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
|
||||
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
||||
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
||||
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
||||
-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
||||
-DCMAKE_BUILD_TYPE:STRING=Release
|
||||
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
|
||||
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build/</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString">all</value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString">clean</value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Release</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString"></value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
||||
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.1">
|
||||
<value type="QString" key="CMake.Build.Type">Debug</value>
|
||||
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
||||
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
||||
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
|
||||
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
|
||||
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
||||
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
||||
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
||||
-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
||||
-DCMAKE_BUILD_TYPE:STRING=Debug
|
||||
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}</value>
|
||||
<value type="QString" key="CMake.Source.Directory">/mnt/share/torsten/Programs/yourpart-daemon</value>
|
||||
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString">all</value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString">clean</value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Debug (importiert)</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">-1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString">install</value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
||||
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">0</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">2</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString"></value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
|
||||
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Application Manager-Paket installieren</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
|
||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
|
||||
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
||||
</valuemap>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.TargetCount</variable>
|
||||
<value type="qlonglong">1</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
|
||||
<value type="int">22</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>Version</variable>
|
||||
<value type="int">22</value>
|
||||
</data>
|
||||
</qtcreator>
|
||||
205
CMakeLists.txt.user.d36652f
Normal file
205
CMakeLists.txt.user.d36652f
Normal file
@@ -0,0 +1,205 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE QtCreatorProject>
|
||||
<!-- Written by QtCreator 12.0.2, 2025-07-18T07:45:58. -->
|
||||
<qtcreator>
|
||||
<data>
|
||||
<variable>EnvironmentId</variable>
|
||||
<value type="QByteArray">{d36652ff-969b-426b-a63f-1edd325096c5}</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.ActiveTarget</variable>
|
||||
<value type="qlonglong">0</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.EditorSettings</variable>
|
||||
<valuemap type="QVariantMap">
|
||||
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
|
||||
<value type="bool" key="EditorConfiguration.AutoSpacesForTabs">false</value>
|
||||
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
|
||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
|
||||
<value type="QString" key="language">Cpp</value>
|
||||
<valuemap type="QVariantMap" key="value">
|
||||
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
|
||||
</valuemap>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
|
||||
<value type="QString" key="language">QmlJS</value>
|
||||
<valuemap type="QVariantMap" key="value">
|
||||
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
|
||||
</valuemap>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
|
||||
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
|
||||
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
|
||||
<value type="int" key="EditorConfiguration.IndentSize">4</value>
|
||||
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
|
||||
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
|
||||
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
|
||||
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
|
||||
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
|
||||
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
|
||||
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
|
||||
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
|
||||
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
|
||||
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">0</value>
|
||||
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
|
||||
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
|
||||
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
|
||||
<value type="int" key="EditorConfiguration.TabSize">8</value>
|
||||
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
|
||||
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
|
||||
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
|
||||
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
|
||||
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
|
||||
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
|
||||
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
|
||||
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
|
||||
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
|
||||
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
|
||||
</valuemap>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.PluginSettings</variable>
|
||||
<valuemap type="QVariantMap">
|
||||
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
|
||||
<value type="bool" key="AutoTest.Framework.Boost">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.CTest">false</value>
|
||||
<value type="bool" key="AutoTest.Framework.Catch">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.GTest">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
|
||||
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
|
||||
<value type="int" key="AutoTest.RunAfterBuild">0</value>
|
||||
<value type="bool" key="AutoTest.UseGlobal">true</value>
|
||||
<valuemap type="QVariantMap" key="ClangTools">
|
||||
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
|
||||
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
|
||||
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
|
||||
<value type="int" key="ClangTools.ParallelJobs">8</value>
|
||||
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
|
||||
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
|
||||
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
|
||||
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
|
||||
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="CppEditor.QuickFix">
|
||||
<value type="bool" key="UseGlobalSettings">true</value>
|
||||
</valuemap>
|
||||
</valuemap>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.Target.0</variable>
|
||||
<valuemap type="QVariantMap">
|
||||
<value type="QString" key="DeviceType">Desktop</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Importiertes Kit</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Importiertes Kit</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{3c6cfc13-714d-4db1-bd45-b9794643cc67}</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
|
||||
<value type="QString" key="CMake.Build.Type">Debug</value>
|
||||
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
|
||||
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
|
||||
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_GENERATOR:STRING=Unix Makefiles
|
||||
-DCMAKE_BUILD_TYPE:STRING=Build
|
||||
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
|
||||
-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}
|
||||
-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
|
||||
-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
|
||||
-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}</value>
|
||||
<value type="QString" key="CMake.Source.Directory">/home/torsten/Programs/yourpart-daemon</value>
|
||||
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString">all</value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
|
||||
</valuemap>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
|
||||
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
|
||||
<value type="QString">clean</value>
|
||||
</valuelist>
|
||||
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Bereinigen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Bereinigen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
|
||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
|
||||
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
|
||||
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Erstellen</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
|
||||
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Deployment</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
|
||||
</valuemap>
|
||||
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
|
||||
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">1</value>
|
||||
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
|
||||
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
|
||||
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
|
||||
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
|
||||
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
|
||||
<value type="QString" key="Analyzer.Valgrind.ValgrindExecutable">/usr/bin/valgrind</value>
|
||||
<valuelist type="QVariantList" key="CustomOutputParsers"/>
|
||||
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
|
||||
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
|
||||
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">yourpart-daemon</value>
|
||||
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.yourpart-daemon</value>
|
||||
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">yourpart-daemon</value>
|
||||
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
|
||||
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
|
||||
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
|
||||
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/home/torsten/Programs/yourpart-daemon/build</value>
|
||||
</valuemap>
|
||||
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
|
||||
</valuemap>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.TargetCount</variable>
|
||||
<value type="qlonglong">1</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
|
||||
<value type="int">22</value>
|
||||
</data>
|
||||
<data>
|
||||
<variable>Version</variable>
|
||||
<value type="int">22</value>
|
||||
</data>
|
||||
</qtcreator>
|
||||
168
SSL-SETUP.md
Normal file
168
SSL-SETUP.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# SSL/TLS Setup für YourPart Daemon
|
||||
|
||||
Dieses Dokument beschreibt, wie Sie SSL/TLS-Zertifikate für den YourPart Daemon einrichten können.
|
||||
|
||||
## 🚀 Schnellstart
|
||||
|
||||
### 1. Self-Signed Certificate (Entwicklung/Testing)
|
||||
```bash
|
||||
./setup-ssl.sh
|
||||
# Wählen Sie Option 1
|
||||
```
|
||||
|
||||
### 2. Let's Encrypt Certificate (Produktion)
|
||||
```bash
|
||||
./setup-ssl.sh
|
||||
# Wählen Sie Option 2
|
||||
```
|
||||
|
||||
### 3. Apache2-Zertifikate verwenden (empfohlen für Ubuntu)
|
||||
```bash
|
||||
./setup-ssl.sh
|
||||
# Wählen Sie Option 4
|
||||
# Verwendet bereits vorhandene Apache2-Zertifikate
|
||||
# ⚠️ Warnung bei Snakeoil-Zertifikaten (nur für localhost)
|
||||
```
|
||||
|
||||
### 4. DNS-01 Challenge (für komplexe Setups)
|
||||
```bash
|
||||
./setup-ssl-dns.sh
|
||||
# Für Cloudflare, Route53, etc.
|
||||
```
|
||||
|
||||
## 📋 Voraussetzungen
|
||||
|
||||
### Für Apache2-Zertifikate:
|
||||
- Apache2 installiert oder Zertifikate in Standard-Pfaden
|
||||
- Unterstützte Pfade (priorisiert nach Qualität):
|
||||
- `/etc/letsencrypt/live/your-part.de/fullchain.pem` (Let's Encrypt - empfohlen)
|
||||
- `/etc/letsencrypt/live/$(hostname)/fullchain.pem` (Let's Encrypt)
|
||||
- `/etc/apache2/ssl/apache.crt` (Custom Apache2)
|
||||
- `/etc/ssl/certs/ssl-cert-snakeoil.pem` (Ubuntu Standard - nur localhost)
|
||||
|
||||
### Für Let's Encrypt (HTTP-01 Challenge):
|
||||
- Port 80 muss verfügbar sein
|
||||
- Domain `your-part.de` muss auf den Server zeigen
|
||||
- Kein anderer Service auf Port 80
|
||||
|
||||
### Für DNS-01 Challenge:
|
||||
- DNS-Provider Account (Cloudflare, Route53, etc.)
|
||||
- API-Credentials für DNS-Management
|
||||
|
||||
## 🔧 Konfiguration
|
||||
|
||||
Nach der Zertifikats-Erstellung:
|
||||
|
||||
1. **SSL in der Konfiguration aktivieren:**
|
||||
```ini
|
||||
# /etc/yourpart/daemon.conf
|
||||
WEBSOCKET_SSL_ENABLED=true
|
||||
WEBSOCKET_SSL_CERT_PATH=/etc/yourpart/server.crt
|
||||
WEBSOCKET_SSL_KEY_PATH=/etc/yourpart/server.key
|
||||
```
|
||||
|
||||
2. **Daemon neu starten:**
|
||||
```bash
|
||||
sudo systemctl restart yourpart-daemon
|
||||
```
|
||||
|
||||
3. **Verbindung testen:**
|
||||
```bash
|
||||
# WebSocket Secure
|
||||
wss://your-part.de:4551
|
||||
|
||||
# Oder ohne SSL
|
||||
ws://your-part.de:4551
|
||||
```
|
||||
|
||||
## 🔄 Automatische Erneuerung
|
||||
|
||||
### Let's Encrypt-Zertifikate:
|
||||
- **Cron Job:** Täglich um 2:30 Uhr
|
||||
- **Script:** `/etc/yourpart/renew-ssl.sh`
|
||||
- **Log:** `/var/log/yourpart/ssl-renewal.log`
|
||||
|
||||
### Apache2-Zertifikate:
|
||||
- **Ubuntu Snakeoil:** Automatisch von Apache2 verwaltet
|
||||
- **Let's Encrypt:** Automatische Erneuerung wenn erkannt
|
||||
- **Custom:** Manuelle Verwaltung erforderlich
|
||||
|
||||
## 📁 Dateistruktur
|
||||
|
||||
```
|
||||
/etc/yourpart/
|
||||
├── server.crt # Zertifikat (Symlink zu Let's Encrypt)
|
||||
├── server.key # Private Key (Symlink zu Let's Encrypt)
|
||||
├── renew-ssl.sh # Auto-Renewal Script
|
||||
└── cloudflare.ini # Cloudflare Credentials (falls verwendet)
|
||||
|
||||
/etc/letsencrypt/live/your-part.de/
|
||||
├── fullchain.pem # Vollständige Zertifikatskette
|
||||
├── privkey.pem # Private Key
|
||||
├── cert.pem # Zertifikat
|
||||
└── chain.pem # Intermediate Certificate
|
||||
```
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Zertifikat wird nicht akzeptiert
|
||||
```bash
|
||||
# Prüfe Zertifikats-Gültigkeit
|
||||
openssl x509 -in /etc/yourpart/server.crt -text -noout
|
||||
|
||||
# Prüfe Berechtigungen
|
||||
ls -la /etc/yourpart/server.*
|
||||
```
|
||||
|
||||
### Let's Encrypt Challenge fehlgeschlagen
|
||||
```bash
|
||||
# Prüfe Port 80
|
||||
sudo netstat -tlnp | grep :80
|
||||
|
||||
# Prüfe DNS
|
||||
nslookup your-part.de
|
||||
|
||||
# Prüfe Firewall
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
### Auto-Renewal funktioniert nicht
|
||||
```bash
|
||||
# Prüfe Cron Jobs
|
||||
sudo crontab -l
|
||||
|
||||
# Teste Renewal Script
|
||||
sudo /etc/yourpart/renew-ssl.sh
|
||||
|
||||
# Prüfe Logs
|
||||
tail -f /var/log/yourpart/ssl-renewal.log
|
||||
```
|
||||
|
||||
## 🔒 Sicherheit
|
||||
|
||||
### Berechtigungen
|
||||
- **Zertifikat:** `644` (readable by all, writable by owner)
|
||||
- **Private Key:** `600` (readable/writable by owner only)
|
||||
- **Owner:** `yourpart:yourpart`
|
||||
|
||||
### Firewall
|
||||
```bash
|
||||
# Öffne Port 80 für Let's Encrypt Challenge
|
||||
sudo ufw allow 80/tcp
|
||||
|
||||
# Öffne Port 4551 für WebSocket
|
||||
sudo ufw allow 4551/tcp
|
||||
```
|
||||
|
||||
## 📚 Weitere Informationen
|
||||
|
||||
- [Let's Encrypt Dokumentation](https://letsencrypt.org/docs/)
|
||||
- [Certbot Dokumentation](https://certbot.eff.org/docs/)
|
||||
- [libwebsockets SSL](https://libwebsockets.org/lws-api-doc-master/html/group__ssl.html)
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
Bei Problemen:
|
||||
1. Prüfen Sie die Logs: `sudo journalctl -u yourpart-daemon -f`
|
||||
2. Testen Sie die Zertifikate: `openssl s_client -connect your-part.de:4551`
|
||||
3. Prüfen Sie die Firewall: `sudo ufw status`
|
||||
184
backend/analyze-indexes.js
Executable file
184
backend/analyze-indexes.js
Executable file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script zur Analyse und Empfehlung von Indizes
|
||||
*
|
||||
* Analysiert:
|
||||
* - Tabellen mit vielen Sequential Scans
|
||||
* - Fehlende Composite Indizes für häufige JOINs
|
||||
* - Ungenutzte Indizes
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🔍 Index-Analyse und Empfehlungen\n');
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
// 1. Tabellen mit vielen Sequential Scans
|
||||
await analyzeSequentialScans();
|
||||
|
||||
// 2. Prüfe häufige JOIN-Patterns
|
||||
await analyzeJoinPatterns();
|
||||
|
||||
// 3. Ungenutzte Indizes
|
||||
await analyzeUnusedIndexes();
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('✅ Analyse abgeschlossen\n');
|
||||
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeSequentialScans() {
|
||||
console.log('📊 1. Tabellen mit vielen Sequential Scans\n');
|
||||
|
||||
const [tables] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || relname as table_name,
|
||||
seq_scan,
|
||||
seq_tup_read,
|
||||
idx_scan,
|
||||
seq_tup_read / NULLIF(seq_scan, 0) as avg_rows_per_scan,
|
||||
CASE
|
||||
WHEN seq_scan + idx_scan > 0
|
||||
THEN round((seq_scan::numeric / (seq_scan + idx_scan)) * 100, 2)
|
||||
ELSE 0
|
||||
END as seq_scan_percent
|
||||
FROM pg_stat_user_tables
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
AND seq_scan > 1000
|
||||
ORDER BY seq_tup_read DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (tables.length > 0) {
|
||||
console.log(' ⚠️ Tabellen mit vielen Sequential Scans:');
|
||||
tables.forEach(t => {
|
||||
console.log(`\n ${t.table_name}:`);
|
||||
console.log(` Sequential Scans: ${parseInt(t.seq_scan).toLocaleString()}`);
|
||||
console.log(` Zeilen gelesen: ${parseInt(t.seq_tup_read).toLocaleString()}`);
|
||||
console.log(` Index Scans: ${parseInt(t.idx_scan).toLocaleString()}`);
|
||||
console.log(` Seq Scan Anteil: ${t.seq_scan_percent}%`);
|
||||
console.log(` Ø Zeilen pro Scan: ${parseInt(t.avg_rows_per_scan).toLocaleString()}`);
|
||||
|
||||
if (t.seq_scan_percent > 50) {
|
||||
console.log(` ⚠️ KRITISCH: Mehr als 50% Sequential Scans!`);
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeJoinPatterns() {
|
||||
console.log('🔗 2. Analyse häufiger JOIN-Patterns\n');
|
||||
|
||||
// Prüfe welche Indizes auf knowledge existieren
|
||||
const [knowledgeIndexes] = await sequelize.query(`
|
||||
SELECT
|
||||
indexname,
|
||||
indexdef
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'falukant_data'
|
||||
AND tablename = 'knowledge'
|
||||
ORDER BY indexname;
|
||||
`);
|
||||
|
||||
console.log(' Indizes auf falukant_data.knowledge:');
|
||||
if (knowledgeIndexes.length > 0) {
|
||||
knowledgeIndexes.forEach(idx => {
|
||||
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' Keine Indizes gefunden');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Empfehlung: Composite Index auf (character_id, product_id)
|
||||
const [knowledgeUsage] = await sequelize.query(`
|
||||
SELECT
|
||||
idx_scan,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname = 'falukant_data'
|
||||
AND relname = 'knowledge'
|
||||
AND indexrelname = 'idx_knowledge_character_id';
|
||||
`);
|
||||
|
||||
if (knowledgeUsage.length > 0) {
|
||||
const usage = knowledgeUsage[0];
|
||||
console.log(' Aktuelle Nutzung von idx_knowledge_character_id:');
|
||||
console.log(` Scans: ${parseInt(usage.idx_scan).toLocaleString()}`);
|
||||
console.log(` Zeilen gelesen: ${parseInt(usage.idx_tup_read).toLocaleString()}`);
|
||||
console.log('');
|
||||
|
||||
console.log(' 💡 Empfehlung:');
|
||||
console.log(' CREATE INDEX IF NOT EXISTS idx_knowledge_character_product');
|
||||
console.log(' ON falukant_data.knowledge(character_id, product_id);');
|
||||
console.log(' → Wird häufig für JOINs mit character_id UND product_id verwendet\n');
|
||||
}
|
||||
|
||||
// Prüfe character Indizes
|
||||
const [characterIndexes] = await sequelize.query(`
|
||||
SELECT
|
||||
indexname,
|
||||
indexdef
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'falukant_data'
|
||||
AND tablename = 'character'
|
||||
ORDER BY indexname;
|
||||
`);
|
||||
|
||||
console.log(' Indizes auf falukant_data.character:');
|
||||
if (characterIndexes.length > 0) {
|
||||
characterIndexes.forEach(idx => {
|
||||
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async function analyzeUnusedIndexes() {
|
||||
console.log('🗑️ 3. Ungenutzte Indizes\n');
|
||||
|
||||
const [unused] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || indexrelname as index_name,
|
||||
schemaname || '.' || relname as table_name,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
||||
idx_scan as scans,
|
||||
pg_relation_size(indexrelid) as size_bytes
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
AND idx_scan = 0
|
||||
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
|
||||
ORDER BY pg_relation_size(indexrelid) DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (unused.length > 0) {
|
||||
console.log(' ⚠️ Ungenutzte Indizes (> 1MB):');
|
||||
unused.forEach(idx => {
|
||||
console.log(` ${idx.index_name} auf ${idx.table_name}`);
|
||||
console.log(` Größe: ${idx.index_size}, Scans: ${idx.scans}`);
|
||||
});
|
||||
console.log('');
|
||||
console.log(' 💡 Überlege, ob diese Indizes gelöscht werden können:');
|
||||
console.log(' DROP INDEX IF EXISTS <index_name>;');
|
||||
console.log('');
|
||||
} else {
|
||||
console.log(' ✅ Keine großen ungenutzten Indizes gefunden\n');
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -12,6 +12,7 @@ import socialnetworkRouter from './routers/socialnetworkRouter.js';
|
||||
import forumRouter from './routers/forumRouter.js';
|
||||
import falukantRouter from './routers/falukantRouter.js';
|
||||
import friendshipRouter from './routers/friendshipRouter.js';
|
||||
import modelsProxyRouter from './routers/modelsProxyRouter.js';
|
||||
import blogRouter from './routers/blogRouter.js';
|
||||
import match3Router from './routers/match3Router.js';
|
||||
import taxiRouter from './routers/taxiRouter.js';
|
||||
@@ -19,6 +20,9 @@ import taxiMapRouter from './routers/taxiMapRouter.js';
|
||||
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
|
||||
import termineRouter from './routers/termineRouter.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 './jobs/sessionCleanup.js';
|
||||
|
||||
@@ -74,11 +78,22 @@ app.use('/api/vocab', vocabRouter);
|
||||
app.use('/api/forum', forumRouter);
|
||||
app.use('/api/falukant', falukantRouter);
|
||||
app.use('/api/friendships', friendshipRouter);
|
||||
app.use('/api/models', modelsProxyRouter);
|
||||
app.use('/api/blog', blogRouter);
|
||||
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
|
||||
// /models/* nicht statisch ausliefern – nur über /api/models (Proxy mit Komprimierung)
|
||||
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.get(/^\/(?!api\/).*/, (req, res) => {
|
||||
res.sendFile(path.join(frontendDir, 'dist', 'index.html'));
|
||||
|
||||
86
backend/check-connections.js
Normal file
86
backend/check-connections.js
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script zum Prüfen und Bereinigen von PostgreSQL-Verbindungen
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🔍 Prüfe PostgreSQL-Verbindungen...\n');
|
||||
|
||||
// Prüfe aktive Verbindungen
|
||||
const [connections] = await sequelize.query(`
|
||||
SELECT
|
||||
count(*) as total,
|
||||
count(*) FILTER (WHERE state = 'active') as active,
|
||||
count(*) FILTER (WHERE state = 'idle') as idle,
|
||||
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
|
||||
count(*) FILTER (WHERE usename = current_user) as my_connections
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database();
|
||||
`);
|
||||
|
||||
console.log('📊 Verbindungsstatistik:');
|
||||
console.log(` Gesamt: ${connections[0].total}`);
|
||||
console.log(` Aktiv: ${connections[0].active}`);
|
||||
console.log(` Idle: ${connections[0].idle}`);
|
||||
console.log(` Idle in Transaction: ${connections[0].idle_in_transaction}`);
|
||||
console.log(` Meine Verbindungen: ${connections[0].my_connections}\n`);
|
||||
|
||||
// Prüfe max_connections Limit
|
||||
const [maxConn] = await sequelize.query(`
|
||||
SELECT setting::int as max_connections
|
||||
FROM pg_settings
|
||||
WHERE name = 'max_connections';
|
||||
`);
|
||||
console.log(`📈 Max Connections Limit: ${maxConn[0].max_connections}`);
|
||||
console.log(`📉 Verfügbare Connections: ${maxConn[0].max_connections - connections[0].total}\n`);
|
||||
|
||||
// Zeige alte idle Verbindungen
|
||||
const [oldConnections] = await sequelize.query(`
|
||||
SELECT
|
||||
pid,
|
||||
usename,
|
||||
application_name,
|
||||
state,
|
||||
state_change,
|
||||
now() - state_change as idle_duration,
|
||||
query
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database()
|
||||
AND state = 'idle'
|
||||
AND state_change < now() - interval '1 minute'
|
||||
ORDER BY state_change ASC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (oldConnections.length > 0) {
|
||||
console.log(`⚠️ Gefunden ${oldConnections.length} alte idle Verbindungen (> 1 Minute):`);
|
||||
oldConnections.forEach(conn => {
|
||||
console.log(` PID: ${conn.pid}, User: ${conn.usename}, Idle seit: ${conn.idle_duration}`);
|
||||
});
|
||||
console.log('\n💡 Tipp: Du kannst alte Verbindungen beenden mit:');
|
||||
console.log(' SELECT pg_terminate_backend(pid) FROM pg_stat_activity');
|
||||
console.log(' WHERE datname = current_database() AND state = \'idle\' AND state_change < now() - interval \'5 minutes\';\n');
|
||||
}
|
||||
|
||||
// Prüfe ob wir nahe am Limit sind
|
||||
const usagePercent = (connections[0].total / maxConn[0].max_connections) * 100;
|
||||
if (usagePercent > 80) {
|
||||
console.log(`⚠️ WARNUNG: ${usagePercent.toFixed(1)}% der verfügbaren Verbindungen werden verwendet!`);
|
||||
console.log(' Es könnte sein, dass nicht genug Verbindungen verfügbar sind.\n');
|
||||
}
|
||||
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
142
backend/check-knowledge-pkey.js
Executable file
142
backend/check-knowledge-pkey.js
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script zur Analyse des knowledge_pkey Problems
|
||||
*
|
||||
* Prüft warum knowledge_pkey nicht verwendet wird
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🔍 Analyse knowledge_pkey Problem\n');
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
// Prüfe ob knowledge einen Primary Key hat
|
||||
const [pkInfo] = await sequelize.query(`
|
||||
SELECT
|
||||
a.attname as column_name,
|
||||
t.conname as constraint_name,
|
||||
t.contype as constraint_type
|
||||
FROM pg_constraint t
|
||||
JOIN pg_class c ON c.oid = t.conrelid
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(t.conkey)
|
||||
WHERE n.nspname = 'falukant_data'
|
||||
AND c.relname = 'knowledge'
|
||||
AND t.contype = 'p';
|
||||
`);
|
||||
|
||||
console.log('📋 Primary Key Information:');
|
||||
if (pkInfo.length > 0) {
|
||||
pkInfo.forEach(pk => {
|
||||
console.log(` Constraint: ${pk.constraint_name}`);
|
||||
console.log(` Spalte: ${pk.column_name}`);
|
||||
console.log(` Typ: ${pk.constraint_type === 'p' ? 'PRIMARY KEY' : pk.constraint_type}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' ⚠️ Kein Primary Key gefunden!');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Prüfe alle Indizes auf knowledge
|
||||
const [allIndexes] = await sequelize.query(`
|
||||
SELECT
|
||||
indexname,
|
||||
indexdef,
|
||||
idx_scan,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch
|
||||
FROM pg_indexes
|
||||
LEFT JOIN pg_stat_user_indexes
|
||||
ON pg_stat_user_indexes.indexrelname = pg_indexes.indexname
|
||||
AND pg_stat_user_indexes.schemaname = pg_indexes.schemaname
|
||||
WHERE pg_indexes.schemaname = 'falukant_data'
|
||||
AND pg_indexes.tablename = 'knowledge'
|
||||
ORDER BY indexname;
|
||||
`);
|
||||
|
||||
console.log('📊 Alle Indizes auf knowledge:');
|
||||
allIndexes.forEach(idx => {
|
||||
console.log(`\n ${idx.indexname}:`);
|
||||
console.log(` Definition: ${idx.indexdef}`);
|
||||
console.log(` Scans: ${idx.idx_scan ? parseInt(idx.idx_scan).toLocaleString() : 'N/A'}`);
|
||||
console.log(` Zeilen gelesen: ${idx.idx_tup_read ? parseInt(idx.idx_tup_read).toLocaleString() : 'N/A'}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Prüfe Tabellenstruktur
|
||||
const [tableStructure] = await sequelize.query(`
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'knowledge'
|
||||
ORDER BY ordinal_position;
|
||||
`);
|
||||
|
||||
console.log('📋 Tabellenstruktur:');
|
||||
tableStructure.forEach(col => {
|
||||
console.log(` ${col.column_name}: ${col.data_type} ${col.is_nullable === 'NO' ? 'NOT NULL' : 'NULL'}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Erklärung: Warum knowledge_pkey ungenutzt ist
|
||||
const pkUnused = allIndexes.find(i => i.indexname === 'knowledge_pkey' && (i.idx_scan == null || parseInt(i.idx_scan) === 0));
|
||||
if (pkUnused) {
|
||||
console.log('💡 Warum knowledge_pkey (0 Scans) ungenutzt ist:');
|
||||
console.log(' Alle Zugriffe filtern nach (character_id, product_id), nie nach id.');
|
||||
console.log(' Der PK-Index wird nur für Eindeutigkeit/Referenzen genutzt, nicht für Lookups.');
|
||||
console.log(' idx_knowledge_character_product deckt die tatsächlichen Queries ab.\n');
|
||||
}
|
||||
|
||||
// Prüfe ob Queries mit id (Primary Key) gemacht werden
|
||||
let idUsage = [];
|
||||
try {
|
||||
const [rows] = await sequelize.query(`
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
total_exec_time,
|
||||
mean_exec_time
|
||||
FROM pg_stat_statements
|
||||
WHERE query LIKE '%knowledge%'
|
||||
AND (query LIKE '%knowledge.id%' OR query LIKE '%knowledge%id%')
|
||||
ORDER BY calls DESC
|
||||
LIMIT 5;
|
||||
`);
|
||||
idUsage = rows;
|
||||
} catch (e) {
|
||||
console.log(' ℹ️ pg_stat_statements nicht verfügbar – keine Query-Statistik.\n');
|
||||
}
|
||||
|
||||
if (idUsage.length > 0) {
|
||||
console.log('🔍 Queries die knowledge.id verwenden:');
|
||||
idUsage.forEach(q => {
|
||||
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}`);
|
||||
console.log(` Query: ${q.query.substring(0, 150)}...`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('pg_stat_statements')) {
|
||||
console.log(' ⚠️ pg_stat_statements ist nicht aktiviert oder nicht verfügbar\n');
|
||||
} else {
|
||||
console.error('❌ Fehler:', error.message);
|
||||
console.error(error.stack);
|
||||
}
|
||||
await sequelize.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
55
backend/cleanup-connections.js
Normal file
55
backend/cleanup-connections.js
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script zum Bereinigen von alten/idle PostgreSQL-Verbindungen
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🧹 Bereinige alte PostgreSQL-Verbindungen...\n');
|
||||
|
||||
// Beende idle Verbindungen, die älter als 5 Minuten sind (außer unserer eigenen)
|
||||
const [result] = await sequelize.query(`
|
||||
SELECT pg_terminate_backend(pid) as terminated
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database()
|
||||
AND pid <> pg_backend_pid()
|
||||
AND state = 'idle'
|
||||
AND state_change < now() - interval '5 minutes';
|
||||
`);
|
||||
|
||||
const terminated = result.filter(r => r.terminated).length;
|
||||
console.log(`✅ ${terminated} alte idle Verbindungen wurden beendet\n`);
|
||||
|
||||
// Zeige verbleibende Verbindungen
|
||||
const [connections] = await sequelize.query(`
|
||||
SELECT
|
||||
count(*) as total,
|
||||
count(*) FILTER (WHERE state = 'active') as active,
|
||||
count(*) FILTER (WHERE state = 'idle') as idle
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database();
|
||||
`);
|
||||
|
||||
console.log('📊 Verbleibende Verbindungen:');
|
||||
console.log(` Gesamt: ${connections[0].total}`);
|
||||
console.log(` Aktiv: ${connections[0].active}`);
|
||||
console.log(` Idle: ${connections[0].idle}\n`);
|
||||
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler:', error.message);
|
||||
if (error.message.includes('SUPERUSER')) {
|
||||
console.error('\n💡 Tipp: Du benötigst Superuser-Rechte oder musst warten, bis Verbindungen freigegeben werden.');
|
||||
console.error(' Versuche es in ein paar Minuten erneut.');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
203
backend/controllers/calendarController.js
Normal file
203
backend/controllers/calendarController.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import calendarService from '../services/calendarService.js';
|
||||
|
||||
function getHashedUserId(req) {
|
||||
return req.headers?.userid;
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* GET /api/calendar/events
|
||||
* Get all events for the authenticated user
|
||||
* Query params: startDate, endDate (optional)
|
||||
*/
|
||||
async getEvents(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
const events = await calendarService.getEvents(hashedUserId, { startDate, endDate });
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
console.error('Calendar getEvents:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/calendar/events/:id
|
||||
* Get a single event by ID
|
||||
*/
|
||||
async getEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const event = await calendarService.getEvent(hashedUserId, req.params.id);
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error('Calendar getEvent:', error);
|
||||
if (error.message === 'Event not found') {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/calendar/events
|
||||
* Create a new event
|
||||
*/
|
||||
async createEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const eventData = req.body;
|
||||
if (!eventData.title || !eventData.startDate) {
|
||||
return res.status(400).json({ error: 'Title and startDate are required' });
|
||||
}
|
||||
|
||||
const event = await calendarService.createEvent(hashedUserId, eventData);
|
||||
res.status(201).json(event);
|
||||
} catch (error) {
|
||||
console.error('Calendar createEvent:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/calendar/events/:id
|
||||
* Update an existing event
|
||||
*/
|
||||
async updateEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const eventData = req.body;
|
||||
if (!eventData.title || !eventData.startDate) {
|
||||
return res.status(400).json({ error: 'Title and startDate are required' });
|
||||
}
|
||||
|
||||
const event = await calendarService.updateEvent(hashedUserId, req.params.id, eventData);
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error('Calendar updateEvent:', error);
|
||||
if (error.message === 'Event not found') {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/calendar/events/:id
|
||||
* Delete an event
|
||||
*/
|
||||
async deleteEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await calendarService.deleteEvent(hashedUserId, req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Calendar deleteEvent:', error);
|
||||
if (error.message === 'Event not found') {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/calendar/birthdays
|
||||
* Get friends' birthdays for a given year
|
||||
* Query params: year (required)
|
||||
*/
|
||||
async getFriendsBirthdays(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||
const birthdays = await calendarService.getFriendsBirthdays(hashedUserId, year);
|
||||
res.json(birthdays);
|
||||
} catch (error) {
|
||||
console.error('Calendar getFriendsBirthdays:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/calendar/widget/birthdays
|
||||
* Get upcoming birthdays for widget display
|
||||
*/
|
||||
async getWidgetBirthdays(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const birthdays = await calendarService.getUpcomingBirthdays(hashedUserId, limit);
|
||||
res.json(birthdays);
|
||||
} catch (error) {
|
||||
console.error('Calendar getWidgetBirthdays:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/calendar/widget/upcoming
|
||||
* Get upcoming events for widget display
|
||||
*/
|
||||
async getWidgetUpcoming(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const events = await calendarService.getUpcomingEvents(hashedUserId, limit);
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
console.error('Calendar getWidgetUpcoming:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/calendar/widget/mini
|
||||
* Get mini calendar data for widget display
|
||||
*/
|
||||
async getWidgetMiniCalendar(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await calendarService.getMiniCalendarData(hashedUserId);
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
console.error('Calendar getWidgetMiniCalendar:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
50
backend/controllers/dashboardController.js
Normal file
50
backend/controllers/dashboardController.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import dashboardService from '../services/dashboardService.js';
|
||||
|
||||
function getHashedUserId(req) {
|
||||
return req.headers?.userid;
|
||||
}
|
||||
|
||||
export default {
|
||||
/** Liste der möglichen Widget-Typen (öffentlich, keine Auth nötig wenn gewünscht – aktuell mit Auth). */
|
||||
async getAvailableWidgets(req, res) {
|
||||
try {
|
||||
const list = await dashboardService.getAvailableWidgets();
|
||||
res.json(list);
|
||||
} catch (error) {
|
||||
console.error('Dashboard getAvailableWidgets:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
async getConfig(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
try {
|
||||
const config = await dashboardService.getConfig(hashedUserId);
|
||||
res.json(config);
|
||||
} catch (error) {
|
||||
console.error('Dashboard getConfig:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
async setConfig(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const config = req.body;
|
||||
if (!config || typeof config !== 'object') {
|
||||
return res.status(400).json({ error: 'Invalid config' });
|
||||
}
|
||||
try {
|
||||
const result = await dashboardService.setConfig(hashedUserId, config);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Dashboard setConfig:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -56,6 +56,10 @@ class FalukantController {
|
||||
if (!page) page = 1;
|
||||
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.buyStorage = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, amount, stockTypeId } = req.body;
|
||||
@@ -93,17 +97,35 @@ class FalukantController {
|
||||
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.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||
this.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) => {
|
||||
console.log('🔍 getGifts called with userId:', userId);
|
||||
return this.service.getGifts(userId);
|
||||
});
|
||||
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
|
||||
this.sendGift = this._wrapWithUser((userId, req) => this.service.sendGift(userId, req.body.giftId));
|
||||
this.sendGift = this._wrapWithUser(async (userId, req) => {
|
||||
try {
|
||||
return await this.service.sendGift(userId, req.body.giftId);
|
||||
} catch (e) {
|
||||
if (e && e.name === 'PreconditionError' && e.message === 'tooOften') {
|
||||
throw { status: 412, message: 'tooOften', retryAt: e.meta?.retryAt };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
|
||||
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
|
||||
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
|
||||
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
|
||||
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
|
||||
@@ -118,17 +140,22 @@ class FalukantController {
|
||||
}, { successStatus: 201 });
|
||||
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.baptise = this._wrapWithUser((userId, req) => {
|
||||
const { characterId: childId, firstName } = req.body;
|
||||
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.sendToSchool = this._wrapWithUser((userId, req) => {
|
||||
@@ -144,7 +171,16 @@ class FalukantController {
|
||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
|
||||
|
||||
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
|
||||
this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr));
|
||||
this.healthActivity = this._wrapWithUser(async (userId, req) => {
|
||||
try {
|
||||
return await this.service.healthActivity(userId, req.body.measureTr);
|
||||
} catch (e) {
|
||||
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
|
||||
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||
@@ -162,6 +198,13 @@ class FalukantController {
|
||||
}
|
||||
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) => {
|
||||
const productId = parseInt(req.query.productId, 10);
|
||||
const currentPrice = parseFloat(req.query.currentPrice);
|
||||
@@ -171,6 +214,16 @@ class FalukantController {
|
||||
}
|
||||
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.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||
|
||||
@@ -178,6 +231,7 @@ class FalukantController {
|
||||
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.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.searchUsers = this._wrapWithUser((userId, req) => {
|
||||
@@ -252,8 +306,14 @@ class FalukantController {
|
||||
} catch (error) {
|
||||
console.error('Controller error:', error);
|
||||
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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,6 @@ const menuStructure = {
|
||||
visible: ["all"],
|
||||
path: "/socialnetwork/gallery"
|
||||
},
|
||||
vocabtrainer: {
|
||||
visible: ["all"],
|
||||
path: "/socialnetwork/vocab",
|
||||
children: {}
|
||||
},
|
||||
blockedUsers: {
|
||||
visible: ["all"],
|
||||
path: "/socialnetwork/blocked"
|
||||
@@ -183,6 +178,30 @@ const menuStructure = {
|
||||
}
|
||||
}
|
||||
},
|
||||
personal: {
|
||||
visible: ["all"],
|
||||
icon: "profile16.png",
|
||||
children: {
|
||||
sprachenlernen: {
|
||||
visible: ["all"],
|
||||
children: {
|
||||
vocabtrainer: {
|
||||
visible: ["all"],
|
||||
path: "/socialnetwork/vocab",
|
||||
showVocabLanguages: 1 // Flag für dynamische Sprachen-Liste
|
||||
},
|
||||
sprachkurse: {
|
||||
visible: ["all"],
|
||||
path: "/socialnetwork/vocab/courses"
|
||||
}
|
||||
}
|
||||
},
|
||||
calendar: {
|
||||
visible: ["all"],
|
||||
path: "/personal/calendar"
|
||||
}
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
visible: ["all"],
|
||||
icon: "settings16.png",
|
||||
@@ -377,22 +396,9 @@ class NavigationController {
|
||||
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
|
||||
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
|
||||
|
||||
// Dynamisches Submenü: Treffpunkt → Vokabeltrainer → (Neue Sprache + abonnierte/angelegte)
|
||||
// Wichtig: "Neue Sprache" soll IMMER sichtbar sein – auch wenn die DB-Abfrage (noch) fehlschlägt.
|
||||
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;
|
||||
}
|
||||
// Vokabeltrainer: Sprachen werden im Frontend dynamisch geladen (wie Forum)
|
||||
// Keine children mehr, da das Menü nur 2 Ebenen unterstützt
|
||||
// Das Frontend lädt die Sprachen separat und zeigt sie als submenu2 an
|
||||
|
||||
res.status(200).json(filteredMenu);
|
||||
} catch (error) {
|
||||
|
||||
21
backend/controllers/newsController.js
Normal file
21
backend/controllers/newsController.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import newsService from '../services/newsService.js';
|
||||
|
||||
/**
|
||||
* GET /api/news?counter=0&language=de&category=top
|
||||
* counter = wievieltes News-Widget aufgerufen wird (0, 1, 2, …), damit keine doppelten Artikel.
|
||||
*/
|
||||
export default {
|
||||
async getNews(req, res) {
|
||||
const counter = Math.max(0, parseInt(req.query.counter, 10) || 0);
|
||||
const language = (req.query.language || 'de').slice(0, 10);
|
||||
const category = (req.query.category || 'top').slice(0, 50);
|
||||
|
||||
try {
|
||||
const { results, nextPage } = await newsService.getNews({ counter, language, category });
|
||||
res.json({ results, nextPage });
|
||||
} catch (error) {
|
||||
console.error('News getNews:', error);
|
||||
res.status(500).json({ error: error.message || 'News konnten nicht geladen werden.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,7 @@ class VocabController {
|
||||
this.service = new VocabService();
|
||||
|
||||
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.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));
|
||||
@@ -21,6 +22,39 @@ class VocabController {
|
||||
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.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 } = {}) {
|
||||
|
||||
159
backend/create-performance-indexes.js
Executable file
159
backend/create-performance-indexes.js
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script zum Erstellen von Performance-Indizes
|
||||
*
|
||||
* Erstellt Indizes basierend auf der Analyse häufiger Queries:
|
||||
* - inventory: stock_id
|
||||
* - stock: branch_id
|
||||
* - production: branch_id
|
||||
* - director: employer_user_id
|
||||
* - knowledge: (character_id, product_id) composite
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🔧 Erstelle Performance-Indizes\n');
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
const indexes = [
|
||||
{
|
||||
name: 'idx_knowledge_character_product',
|
||||
table: 'falukant_data.knowledge',
|
||||
columns: '(character_id, product_id)',
|
||||
description: 'Composite Index für JOINs mit character_id UND product_id',
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: 'idx_inventory_stock_id',
|
||||
table: 'falukant_data.inventory',
|
||||
columns: '(stock_id)',
|
||||
description: 'Index für WHERE inventory.stock_id = ...',
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: 'idx_stock_branch_id',
|
||||
table: 'falukant_data.stock',
|
||||
columns: '(branch_id)',
|
||||
description: 'Index für WHERE stock.branch_id = ...',
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: 'idx_production_branch_id',
|
||||
table: 'falukant_data.production',
|
||||
columns: '(branch_id)',
|
||||
description: 'Index für WHERE production.branch_id = ...',
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: 'idx_director_employer_user_id',
|
||||
table: 'falukant_data.director',
|
||||
columns: '(employer_user_id)',
|
||||
description: 'Index für WHERE director.employer_user_id = ...',
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: 'idx_production_start_timestamp',
|
||||
table: 'falukant_data.production',
|
||||
columns: '(start_timestamp)',
|
||||
description: 'Index für WHERE production.start_timestamp <= ...',
|
||||
critical: false
|
||||
},
|
||||
{
|
||||
name: 'idx_director_last_salary_payout',
|
||||
table: 'falukant_data.director',
|
||||
columns: '(last_salary_payout)',
|
||||
description: 'Index für WHERE director.last_salary_payout < ...',
|
||||
critical: false
|
||||
}
|
||||
];
|
||||
|
||||
console.log(`📋 ${indexes.length} Indizes werden erstellt:\n`);
|
||||
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (let i = 0; i < indexes.length; i++) {
|
||||
const idx = indexes[i];
|
||||
const criticalMark = idx.critical ? ' ⚠️ KRITISCH' : '';
|
||||
|
||||
console.log(`[${i + 1}/${indexes.length}] ${idx.name}${criticalMark}`);
|
||||
console.log(` Tabelle: ${idx.table}`);
|
||||
console.log(` Spalten: ${idx.columns}`);
|
||||
console.log(` Beschreibung: ${idx.description}`);
|
||||
|
||||
try {
|
||||
// Prüfe ob Index bereits existiert
|
||||
const [existing] = await sequelize.query(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE schemaname || '.' || tablename = '${idx.table}'
|
||||
AND indexname = '${idx.name}'
|
||||
) as exists;
|
||||
`);
|
||||
|
||||
if (existing[0].exists) {
|
||||
console.log(` ⏭️ Index existiert bereits, überspringe\n`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Erstelle Index
|
||||
const startTime = Date.now();
|
||||
await sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS ${idx.name}
|
||||
ON ${idx.table} USING btree ${idx.columns};
|
||||
`);
|
||||
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(` ✅ Erstellt in ${duration}s\n`);
|
||||
created++;
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ Fehler: ${error.message}\n`);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log(`✅ Zusammenfassung:`);
|
||||
console.log(` Erstellt: ${created}`);
|
||||
console.log(` Übersprungen: ${skipped}`);
|
||||
console.log(` Fehler: ${errors}\n`);
|
||||
|
||||
// ANALYZE ausführen, damit PostgreSQL die neuen Indizes berücksichtigt
|
||||
const tablesToAnalyze = [
|
||||
'falukant_data.knowledge',
|
||||
'falukant_data.inventory',
|
||||
'falukant_data.stock',
|
||||
'falukant_data.production',
|
||||
'falukant_data.director'
|
||||
];
|
||||
if (created > 0) {
|
||||
console.log('📊 Führe ANALYZE auf betroffenen Tabellen aus...\n');
|
||||
for (const table of tablesToAnalyze) {
|
||||
try {
|
||||
await sequelize.query(`ANALYZE ${table};`);
|
||||
console.log(` ✅ ANALYZE ${table};`);
|
||||
} catch (err) {
|
||||
console.log(` ⚠️ ${table}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -25,11 +25,13 @@ function createServer() {
|
||||
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
|
||||
});
|
||||
wss = new WebSocketServer({ server: httpsServer });
|
||||
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
|
||||
httpsServer.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
|
||||
});
|
||||
} else {
|
||||
wss = new WebSocketServer({ port: PORT });
|
||||
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
|
||||
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
|
||||
console.log(`[Daemon] WS (ohne TLS) Server startet auf Port ${PORT} ...`);
|
||||
}
|
||||
|
||||
|
||||
479
backend/diagnose-db-performance.js
Executable file
479
backend/diagnose-db-performance.js
Executable file
@@ -0,0 +1,479 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Umfassendes Diagnose-Script für Datenbank-Performance
|
||||
*
|
||||
* Untersucht:
|
||||
* - Verbindungsstatistiken
|
||||
* - Langsame Queries
|
||||
* - Tabellengrößen und Bloat
|
||||
* - Indizes (fehlende/ungenutzte)
|
||||
* - Vacuum/Analyze Status
|
||||
* - Locking/Blocking
|
||||
* - Query-Statistiken
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🔍 Datenbank-Performance-Diagnose\n');
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
// 1. Verbindungsstatistiken
|
||||
await checkConnections();
|
||||
|
||||
// 2. Langsame Queries (wenn pg_stat_statements aktiviert ist)
|
||||
await checkSlowQueries();
|
||||
|
||||
// 3. Tabellengrößen und Bloat
|
||||
await checkTableSizes();
|
||||
|
||||
// 4. Indizes prüfen
|
||||
await checkIndexes();
|
||||
|
||||
// 5. Vacuum/Analyze Status
|
||||
await checkVacuumStatus();
|
||||
|
||||
// 6. Locking/Blocking
|
||||
await checkLocks();
|
||||
|
||||
// 7. Query-Statistiken (wenn pg_stat_statements aktiviert ist)
|
||||
await checkQueryStats();
|
||||
|
||||
// 8. Connection Pool Status
|
||||
await checkConnectionPool();
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('✅ Diagnose abgeschlossen\n');
|
||||
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkConnections() {
|
||||
console.log('📊 1. Verbindungsstatistiken\n');
|
||||
|
||||
const [connections] = await sequelize.query(`
|
||||
SELECT
|
||||
count(*) as total,
|
||||
count(*) FILTER (WHERE state = 'active') as active,
|
||||
count(*) FILTER (WHERE state = 'idle') as idle,
|
||||
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction,
|
||||
count(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database();
|
||||
`);
|
||||
|
||||
const conn = connections[0];
|
||||
console.log(` Gesamt: ${conn.total}`);
|
||||
console.log(` Aktiv: ${conn.active}`);
|
||||
console.log(` Idle: ${conn.idle}`);
|
||||
console.log(` Idle in Transaction: ${conn.idle_in_transaction}`);
|
||||
console.log(` Wartend: ${conn.waiting}\n`);
|
||||
|
||||
const [maxConn] = await sequelize.query(`
|
||||
SELECT setting::int as max_connections
|
||||
FROM pg_settings
|
||||
WHERE name = 'max_connections';
|
||||
`);
|
||||
|
||||
const usagePercent = (conn.total / maxConn[0].max_connections) * 100;
|
||||
console.log(` Max Connections: ${maxConn[0].max_connections}`);
|
||||
console.log(` Auslastung: ${usagePercent.toFixed(1)}%\n`);
|
||||
|
||||
if (usagePercent > 80) {
|
||||
console.log(' ⚠️ WARNUNG: Hohe Verbindungsauslastung!\n');
|
||||
}
|
||||
|
||||
// Zeige lange laufende Queries
|
||||
const [longRunning] = await sequelize.query(`
|
||||
SELECT
|
||||
pid,
|
||||
usename,
|
||||
application_name,
|
||||
state,
|
||||
now() - query_start as duration,
|
||||
wait_event_type,
|
||||
wait_event,
|
||||
left(query, 100) as query_preview
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = current_database()
|
||||
AND state != 'idle'
|
||||
AND now() - query_start > interval '5 seconds'
|
||||
ORDER BY query_start ASC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (longRunning.length > 0) {
|
||||
console.log(' ⚠️ Lange laufende Queries (> 5 Sekunden):');
|
||||
longRunning.forEach(q => {
|
||||
const duration = Math.round(q.duration.total_seconds);
|
||||
console.log(` PID ${q.pid}: ${duration}s - ${q.query_preview}...`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSlowQueries() {
|
||||
console.log('🐌 2. Langsame Queries (pg_stat_statements)\n');
|
||||
|
||||
try {
|
||||
// Prüfe ob pg_stat_statements aktiviert ist
|
||||
const [extension] = await sequelize.query(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
|
||||
) as exists;
|
||||
`);
|
||||
|
||||
if (!extension[0].exists) {
|
||||
console.log(' ℹ️ pg_stat_statements ist nicht aktiviert.');
|
||||
console.log(' 💡 Aktivieren mit: CREATE EXTENSION IF NOT EXISTS pg_stat_statements;\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const [slowQueries] = await sequelize.query(`
|
||||
SELECT
|
||||
left(query, 100) as query_preview,
|
||||
calls,
|
||||
total_exec_time,
|
||||
mean_exec_time,
|
||||
max_exec_time,
|
||||
(total_exec_time / sum(total_exec_time) OVER ()) * 100 as percent_total
|
||||
FROM pg_stat_statements
|
||||
WHERE mean_exec_time > 100 -- Queries mit > 100ms Durchschnitt
|
||||
ORDER BY total_exec_time DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (slowQueries.length > 0) {
|
||||
console.log(' Top 10 langsamste Queries (nach Gesamtzeit):');
|
||||
slowQueries.forEach((q, i) => {
|
||||
console.log(` ${i + 1}. ${q.query_preview}...`);
|
||||
console.log(` Aufrufe: ${q.calls}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms, Max: ${q.max_exec_time.toFixed(2)}ms`);
|
||||
console.log(` Gesamtzeit: ${q.total_exec_time.toFixed(2)}ms (${q.percent_total.toFixed(1)}%)\n`);
|
||||
});
|
||||
} else {
|
||||
console.log(' ✅ Keine sehr langsamen Queries gefunden (> 100ms Durchschnitt)\n');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Fehler beim Abrufen der Query-Statistiken: ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTableSizes() {
|
||||
console.log('📦 3. Tabellengrößen und Bloat\n');
|
||||
|
||||
const [tableSizes] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || relname as full_table_name,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size,
|
||||
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) as table_size,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) as indexes_size,
|
||||
n_live_tup as row_count,
|
||||
n_dead_tup as dead_rows,
|
||||
CASE
|
||||
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
|
||||
ELSE 0
|
||||
END as dead_row_percent,
|
||||
last_vacuum,
|
||||
last_autovacuum,
|
||||
last_analyze,
|
||||
last_autoanalyze
|
||||
FROM pg_stat_user_tables
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC
|
||||
LIMIT 20;
|
||||
`);
|
||||
|
||||
if (tableSizes.length > 0) {
|
||||
console.log(' Top 20 größte Tabellen:');
|
||||
tableSizes.forEach((t, i) => {
|
||||
console.log(` ${i + 1}. ${t.full_table_name}`);
|
||||
console.log(` Größe: ${t.total_size} (Tabelle: ${t.table_size}, Indizes: ${t.indexes_size})`);
|
||||
console.log(` Zeilen: ${parseInt(t.row_count).toLocaleString()}, Tote Zeilen: ${parseInt(t.dead_rows).toLocaleString()} (${t.dead_row_percent}%)`);
|
||||
|
||||
if (parseFloat(t.dead_row_percent) > 20) {
|
||||
console.log(` ⚠️ Hoher Bloat-Anteil! Vacuum empfohlen.`);
|
||||
}
|
||||
|
||||
if (t.last_vacuum || t.last_autovacuum) {
|
||||
const lastVacuum = t.last_vacuum || t.last_autovacuum;
|
||||
const daysSinceVacuum = Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24));
|
||||
if (daysSinceVacuum > 7) {
|
||||
console.log(` ⚠️ Letztes Vacuum: ${daysSinceVacuum} Tage her`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function checkIndexes() {
|
||||
console.log('🔍 4. Indizes-Analyse\n');
|
||||
|
||||
// Fehlende Indizes (basierend auf pg_stat_user_tables)
|
||||
const [missingIndexes] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || relname as table_name,
|
||||
seq_scan,
|
||||
seq_tup_read,
|
||||
idx_scan,
|
||||
seq_tup_read / NULLIF(seq_scan, 0) as avg_seq_read
|
||||
FROM pg_stat_user_tables
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
AND seq_scan > 1000
|
||||
AND seq_tup_read / NULLIF(seq_scan, 0) > 1000
|
||||
ORDER BY seq_tup_read DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (missingIndexes.length > 0) {
|
||||
console.log(' ⚠️ Tabellen mit vielen Sequential Scans (möglicherweise fehlende Indizes):');
|
||||
missingIndexes.forEach(t => {
|
||||
console.log(` ${t.table_name}: ${t.seq_scan} seq scans, ${parseInt(t.seq_tup_read).toLocaleString()} Zeilen gelesen`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Ungenutzte Indizes
|
||||
const [unusedIndexes] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || indexrelname as index_name,
|
||||
schemaname || '.' || relname as table_name,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
||||
idx_scan as scans
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
AND idx_scan = 0
|
||||
AND pg_relation_size(indexrelid) > 1024 * 1024 -- Größer als 1MB
|
||||
ORDER BY pg_relation_size(indexrelid) DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (unusedIndexes.length > 0) {
|
||||
console.log(' ⚠️ Ungenutzte Indizes (> 1MB, nie verwendet):');
|
||||
unusedIndexes.forEach(idx => {
|
||||
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (0 Scans)`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Index Bloat
|
||||
const [indexBloat] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || indexrelname as index_name,
|
||||
schemaname || '.' || relname as table_name,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
|
||||
idx_scan as scans,
|
||||
idx_tup_read as tuples_read,
|
||||
idx_tup_fetch as tuples_fetched
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
AND pg_relation_size(indexrelid) > 10 * 1024 * 1024 -- Größer als 10MB
|
||||
ORDER BY pg_relation_size(indexrelid) DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (indexBloat.length > 0) {
|
||||
console.log(' Top 10 größte Indizes:');
|
||||
indexBloat.forEach(idx => {
|
||||
console.log(` ${idx.index_name} auf ${idx.table_name}: ${idx.index_size} (${idx.scans} Scans)`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkVacuumStatus() {
|
||||
console.log('🧹 5. Vacuum/Analyze Status\n');
|
||||
|
||||
const [vacuumStats] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || relname as table_name,
|
||||
last_vacuum,
|
||||
last_autovacuum,
|
||||
last_analyze,
|
||||
last_autoanalyze,
|
||||
n_dead_tup,
|
||||
n_live_tup,
|
||||
CASE
|
||||
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
|
||||
ELSE 0
|
||||
END as dead_percent
|
||||
FROM pg_stat_user_tables
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
AND (
|
||||
(last_vacuum IS NULL AND last_autovacuum IS NULL)
|
||||
OR (last_vacuum < now() - interval '7 days' AND last_autovacuum < now() - interval '7 days')
|
||||
OR n_dead_tup > 10000
|
||||
)
|
||||
ORDER BY n_dead_tup DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (vacuumStats.length > 0) {
|
||||
console.log(' ⚠️ Tabellen, die Vacuum benötigen könnten:');
|
||||
vacuumStats.forEach(t => {
|
||||
const lastVacuum = t.last_vacuum || t.last_autovacuum || 'Nie';
|
||||
const daysSince = lastVacuum !== 'Nie'
|
||||
? Math.floor((new Date() - new Date(lastVacuum)) / (1000 * 60 * 60 * 24))
|
||||
: '∞';
|
||||
console.log(` ${t.table_name}:`);
|
||||
console.log(` Tote Zeilen: ${parseInt(t.n_dead_tup).toLocaleString()} (${t.dead_percent}%)`);
|
||||
console.log(` Letztes Vacuum: ${lastVacuum} (${daysSince} Tage)`);
|
||||
});
|
||||
console.log('');
|
||||
} else {
|
||||
console.log(' ✅ Alle Tabellen sind aktuell gevacuumt\n');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkLocks() {
|
||||
console.log('🔒 6. Locking/Blocking\n');
|
||||
|
||||
const [locks] = await sequelize.query(`
|
||||
SELECT
|
||||
blocked_locks.pid AS blocked_pid,
|
||||
blocked_activity.usename AS blocked_user,
|
||||
blocking_locks.pid AS blocking_pid,
|
||||
blocking_activity.usename AS blocking_user,
|
||||
blocked_activity.query AS blocked_statement,
|
||||
blocking_activity.query AS blocking_statement,
|
||||
blocked_activity.application_name AS blocked_app,
|
||||
blocking_activity.application_name AS blocking_app
|
||||
FROM pg_catalog.pg_locks blocked_locks
|
||||
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
|
||||
JOIN pg_catalog.pg_locks blocking_locks
|
||||
ON blocking_locks.locktype = blocked_locks.locktype
|
||||
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
|
||||
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
|
||||
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
|
||||
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
|
||||
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
|
||||
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
|
||||
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
|
||||
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
|
||||
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
|
||||
AND blocking_locks.pid != blocked_locks.pid
|
||||
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
|
||||
WHERE NOT blocked_locks.granted;
|
||||
`);
|
||||
|
||||
if (locks.length > 0) {
|
||||
console.log(' ⚠️ Blockierte Queries gefunden:');
|
||||
locks.forEach(lock => {
|
||||
console.log(` Blockiert: PID ${lock.blocked_pid} (${lock.blocked_user})`);
|
||||
console.log(` Blockiert von: PID ${lock.blocking_pid} (${lock.blocking_user})`);
|
||||
console.log(` Blockierte Query: ${lock.blocked_statement.substring(0, 100)}...`);
|
||||
console.log(` Blockierende Query: ${lock.blocking_statement.substring(0, 100)}...\n`);
|
||||
});
|
||||
} else {
|
||||
console.log(' ✅ Keine blockierten Queries gefunden\n');
|
||||
}
|
||||
|
||||
// Zeige alle aktiven Locks
|
||||
const [allLocks] = await sequelize.query(`
|
||||
SELECT
|
||||
locktype,
|
||||
relation::regclass as relation,
|
||||
mode,
|
||||
granted,
|
||||
pid
|
||||
FROM pg_locks
|
||||
WHERE relation IS NOT NULL
|
||||
AND NOT granted
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
if (allLocks.length > 0) {
|
||||
console.log(' ⚠️ Wartende Locks:');
|
||||
allLocks.forEach(lock => {
|
||||
console.log(` ${lock.locktype} auf ${lock.relation}: ${lock.mode} (PID ${lock.pid})`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkQueryStats() {
|
||||
console.log('📈 7. Query-Statistiken\n');
|
||||
|
||||
try {
|
||||
const [extension] = await sequelize.query(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
|
||||
) as exists;
|
||||
`);
|
||||
|
||||
if (!extension[0].exists) {
|
||||
console.log(' ℹ️ pg_stat_statements ist nicht aktiviert.\n');
|
||||
return;
|
||||
}
|
||||
|
||||
const [topQueries] = await sequelize.query(`
|
||||
SELECT
|
||||
left(query, 80) as query_preview,
|
||||
calls,
|
||||
total_exec_time,
|
||||
mean_exec_time,
|
||||
(100 * total_exec_time / sum(total_exec_time) OVER ()) as percent_total
|
||||
FROM pg_stat_statements
|
||||
WHERE query NOT LIKE '%pg_stat_statements%'
|
||||
ORDER BY calls DESC
|
||||
LIMIT 5;
|
||||
`);
|
||||
|
||||
if (topQueries.length > 0) {
|
||||
console.log(' Top 5 häufigste Queries:');
|
||||
topQueries.forEach((q, i) => {
|
||||
console.log(` ${i + 1}. ${q.query_preview}...`);
|
||||
console.log(` Aufrufe: ${parseInt(q.calls).toLocaleString()}, Durchschnitt: ${q.mean_exec_time.toFixed(2)}ms`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Fehler: ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkConnectionPool() {
|
||||
console.log('🏊 8. Connection Pool Status\n');
|
||||
|
||||
try {
|
||||
// Hole Pool-Konfiguration aus Sequelize Config
|
||||
const config = sequelize.config;
|
||||
const poolConfig = config.pool || {};
|
||||
|
||||
console.log(` Pool-Konfiguration:`);
|
||||
console.log(` Max: ${poolConfig.max || 'N/A'}`);
|
||||
console.log(` Min: ${poolConfig.min || 'N/A'}`);
|
||||
console.log(` Acquire Timeout: ${poolConfig.acquire || 'N/A'}ms`);
|
||||
console.log(` Idle Timeout: ${poolConfig.idle || 'N/A'}ms`);
|
||||
console.log(` Evict Interval: ${poolConfig.evict || 'N/A'}ms\n`);
|
||||
|
||||
// Versuche Pool-Status zu bekommen
|
||||
const pool = sequelize.connectionManager.pool;
|
||||
if (pool) {
|
||||
const poolSize = pool.size || 0;
|
||||
const poolUsed = pool.used || 0;
|
||||
const poolPending = pool.pending || 0;
|
||||
|
||||
console.log(` Pool-Status:`);
|
||||
console.log(` Größe: ${poolSize}`);
|
||||
console.log(` Verwendet: ${poolUsed}`);
|
||||
console.log(` Wartend: ${poolPending}\n`);
|
||||
} else {
|
||||
console.log(` ℹ️ Pool-Objekt nicht verfügbar\n`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Fehler beim Abrufen der Pool-Informationen: ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,43 @@
|
||||
-- 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);
|
||||
132
backend/migrations/20260115000000-add-vocab-courses.cjs
Normal file
132
backend/migrations/20260115000000-add-vocab-courses.cjs
Normal file
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable */
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
// Kurs-Tabelle
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course (
|
||||
id SERIAL PRIMARY KEY,
|
||||
owner_user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
language_id INTEGER NOT NULL,
|
||||
difficulty_level INTEGER DEFAULT 1,
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
share_code TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_owner_fk
|
||||
FOREIGN KEY (owner_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_language_fk
|
||||
FOREIGN KEY (language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
|
||||
);
|
||||
`);
|
||||
|
||||
// Lektionen innerhalb eines Kurses
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
|
||||
id SERIAL PRIMARY KEY,
|
||||
course_id INTEGER NOT NULL,
|
||||
chapter_id INTEGER NOT NULL,
|
||||
lesson_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_lesson_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_lesson_chapter_fk
|
||||
FOREIGN KEY (chapter_id)
|
||||
REFERENCES community.vocab_chapter(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
|
||||
);
|
||||
`);
|
||||
|
||||
// Einschreibungen in Kurse
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
course_id INTEGER NOT NULL,
|
||||
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_enrollment_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_enrollment_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
|
||||
);
|
||||
`);
|
||||
|
||||
// Fortschritt pro User und Lektion
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
course_id INTEGER NOT NULL,
|
||||
lesson_id INTEGER NOT NULL,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
score INTEGER DEFAULT 0,
|
||||
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT vocab_course_progress_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_lesson_fk
|
||||
FOREIGN KEY (lesson_id)
|
||||
REFERENCES community.vocab_course_lesson(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
|
||||
);
|
||||
`);
|
||||
|
||||
// Indizes
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
|
||||
ON community.vocab_course(owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
|
||||
ON community.vocab_course(language_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
|
||||
ON community.vocab_course(is_public);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
|
||||
ON community.vocab_course_lesson(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
|
||||
ON community.vocab_course_lesson(chapter_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
|
||||
ON community.vocab_course_enrollment(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
|
||||
ON community.vocab_course_enrollment(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
|
||||
ON community.vocab_course_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
|
||||
ON community.vocab_course_progress(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
|
||||
ON community.vocab_course_progress(lesson_id);
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TABLE IF EXISTS community.vocab_course_progress CASCADE;
|
||||
DROP TABLE IF EXISTS community.vocab_course_enrollment CASCADE;
|
||||
DROP TABLE IF EXISTS community.vocab_course_lesson CASCADE;
|
||||
DROP TABLE IF EXISTS community.vocab_course CASCADE;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/* eslint-disable */
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
// Grammatik-Übungstypen (z.B. "gap_fill", "multiple_choice", "sentence_building", "transformation")
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
// Grammatik-Übungen (verknüpft mit Lektionen)
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
|
||||
id SERIAL PRIMARY KEY,
|
||||
lesson_id INTEGER NOT NULL,
|
||||
exercise_type_id INTEGER NOT NULL,
|
||||
exercise_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
instruction TEXT,
|
||||
question_data JSONB NOT NULL,
|
||||
answer_data JSONB NOT NULL,
|
||||
explanation TEXT,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_grammar_exercise_lesson_fk
|
||||
FOREIGN KEY (lesson_id)
|
||||
REFERENCES community.vocab_course_lesson(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_type_fk
|
||||
FOREIGN KEY (exercise_type_id)
|
||||
REFERENCES community.vocab_grammar_exercise_type(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_creator_fk
|
||||
FOREIGN KEY (created_by_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
|
||||
);
|
||||
`);
|
||||
|
||||
// Fortschritt für Grammatik-Übungen
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
exercise_id INTEGER NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
correct_attempts INTEGER DEFAULT 0,
|
||||
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
|
||||
FOREIGN KEY (exercise_id)
|
||||
REFERENCES community.vocab_grammar_exercise(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
|
||||
);
|
||||
`);
|
||||
|
||||
// Indizes
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
|
||||
ON community.vocab_grammar_exercise(lesson_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
|
||||
ON community.vocab_grammar_exercise(exercise_type_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
|
||||
ON community.vocab_grammar_exercise_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
|
||||
ON community.vocab_grammar_exercise_progress(exercise_id);
|
||||
`);
|
||||
|
||||
// Standard-Übungstypen einfügen
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
||||
('gap_fill', 'Lückentext-Übung'),
|
||||
('multiple_choice', 'Multiple-Choice-Fragen'),
|
||||
('sentence_building', 'Satzbau-Übung'),
|
||||
('transformation', 'Satzumformung'),
|
||||
('conjugation', 'Konjugations-Übung'),
|
||||
('declension', 'Deklinations-Übung')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TABLE IF EXISTS community.vocab_grammar_exercise_progress CASCADE;
|
||||
DROP TABLE IF EXISTS community.vocab_grammar_exercise CASCADE;
|
||||
DROP TABLE IF EXISTS community.vocab_grammar_exercise_type CASCADE;
|
||||
`);
|
||||
}
|
||||
};
|
||||
47
backend/migrations/20260115000002-add-course-structure.cjs
Normal file
47
backend/migrations/20260115000002-add-course-structure.cjs
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable */
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
// chapter_id optional machen (nicht alle Lektionen brauchen ein Kapitel)
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE community.vocab_course_lesson
|
||||
ALTER COLUMN chapter_id DROP NOT NULL;
|
||||
`);
|
||||
|
||||
// Kurs-Wochen/Module hinzufügen
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE community.vocab_course_lesson
|
||||
ADD COLUMN IF NOT EXISTS week_number INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS day_number INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS lesson_type TEXT DEFAULT 'vocab',
|
||||
ADD COLUMN IF NOT EXISTS audio_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS cultural_notes TEXT;
|
||||
`);
|
||||
|
||||
// Indizes für Wochen/Tage
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
|
||||
ON community.vocab_course_lesson(course_id, week_number);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
|
||||
ON community.vocab_course_lesson(lesson_type);
|
||||
`);
|
||||
|
||||
// Kommentar hinzufügen für lesson_type
|
||||
await queryInterface.sequelize.query(`
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
|
||||
'Type: vocab, grammar, conversation, culture, review';
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE community.vocab_course_lesson
|
||||
DROP COLUMN IF EXISTS week_number,
|
||||
DROP COLUMN IF EXISTS day_number,
|
||||
DROP COLUMN IF EXISTS lesson_type,
|
||||
DROP COLUMN IF EXISTS audio_url,
|
||||
DROP COLUMN IF EXISTS cultural_notes;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
// Lernziele für Lektionen
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE community.vocab_course_lesson
|
||||
ADD COLUMN IF NOT EXISTS target_minutes INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS target_score_percent INTEGER DEFAULT 80,
|
||||
ADD COLUMN IF NOT EXISTS requires_review BOOLEAN DEFAULT false;
|
||||
`);
|
||||
|
||||
// Kommentare hinzufügen
|
||||
await queryInterface.sequelize.query(`
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
|
||||
'Zielzeit in Minuten für diese Lektion';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
|
||||
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
|
||||
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE community.vocab_course_lesson
|
||||
DROP COLUMN IF EXISTS target_minutes,
|
||||
DROP COLUMN IF EXISTS target_score_percent,
|
||||
DROP COLUMN IF EXISTS requires_review;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import ChatUser from './chat/user.js';
|
||||
import Room from './chat/room.js';
|
||||
import User from './community/user.js';
|
||||
import UserParam from './community/user_param.js';
|
||||
import UserDashboard from './community/user_dashboard.js';
|
||||
import UserParamType from './type/user_param.js';
|
||||
import UserRightType from './type/user_right.js';
|
||||
import UserRight from './community/user_right.js';
|
||||
@@ -93,6 +94,10 @@ import PoliticalOfficeRequirement from './falukant/predefine/political_office_pr
|
||||
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
|
||||
import PoliticalOfficeHistory from './falukant/log/political_office_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 UndergroundType from './falukant/type/underground.js';
|
||||
import VehicleType from './falukant/type/vehicle.js';
|
||||
@@ -102,8 +107,17 @@ import RegionDistance from './falukant/data/region_distance.js';
|
||||
import WeatherType from './falukant/type/weather.js';
|
||||
import Weather from './falukant/data/weather.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 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 Match3Level from './match3/level.js';
|
||||
import Objective from './match3/objective.js';
|
||||
@@ -155,6 +169,9 @@ export default function setupAssociations() {
|
||||
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
|
||||
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' });
|
||||
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
|
||||
|
||||
@@ -405,6 +422,13 @@ export default function setupAssociations() {
|
||||
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
|
||||
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' });
|
||||
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
|
||||
|
||||
@@ -558,14 +582,14 @@ export default function setupAssociations() {
|
||||
|
||||
Party.belongsToMany(TitleOfNobility, {
|
||||
through: PartyInvitedNobility,
|
||||
foreignKey: 'party_id',
|
||||
otherKey: 'title_of_nobility_id',
|
||||
foreignKey: 'partyId',
|
||||
otherKey: 'titleOfNobilityId',
|
||||
as: 'invitedNobilities',
|
||||
});
|
||||
TitleOfNobility.belongsToMany(Party, {
|
||||
through: PartyInvitedNobility,
|
||||
foreignKey: 'title_of_nobility_id',
|
||||
otherKey: 'party_id',
|
||||
foreignKey: 'titleOfNobilityId',
|
||||
otherKey: 'partyId',
|
||||
as: 'partiesInvitedTo',
|
||||
});
|
||||
|
||||
@@ -859,6 +883,96 @@ export default function setupAssociations() {
|
||||
}
|
||||
);
|
||||
|
||||
// — Church Offices —
|
||||
|
||||
// Requirements for church office
|
||||
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
||||
foreignKey: 'officeTypeId',
|
||||
as: 'officeType'
|
||||
});
|
||||
ChurchOfficeType.hasMany(ChurchOfficeRequirement, {
|
||||
foreignKey: 'officeTypeId',
|
||||
as: 'requirements'
|
||||
});
|
||||
|
||||
// Prerequisite office type
|
||||
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
|
||||
foreignKey: 'prerequisiteOfficeTypeId',
|
||||
as: 'prerequisiteOfficeType'
|
||||
});
|
||||
|
||||
// Actual church office holdings
|
||||
ChurchOffice.belongsTo(ChurchOfficeType, {
|
||||
foreignKey: 'officeTypeId',
|
||||
as: 'type'
|
||||
});
|
||||
ChurchOfficeType.hasMany(ChurchOffice, {
|
||||
foreignKey: 'officeTypeId',
|
||||
as: 'offices'
|
||||
});
|
||||
|
||||
ChurchOffice.belongsTo(FalukantCharacter, {
|
||||
foreignKey: 'characterId',
|
||||
as: 'holder'
|
||||
});
|
||||
FalukantCharacter.hasOne(ChurchOffice, {
|
||||
foreignKey: 'characterId',
|
||||
as: 'heldChurchOffice'
|
||||
});
|
||||
|
||||
// Supervisor relationship
|
||||
ChurchOffice.belongsTo(FalukantCharacter, {
|
||||
foreignKey: 'supervisorId',
|
||||
as: 'supervisor'
|
||||
});
|
||||
|
||||
// Region relationship
|
||||
ChurchOffice.belongsTo(RegionData, {
|
||||
foreignKey: 'regionId',
|
||||
as: 'region'
|
||||
});
|
||||
RegionData.hasMany(ChurchOffice, {
|
||||
foreignKey: 'regionId',
|
||||
as: 'churchOffices'
|
||||
});
|
||||
|
||||
// Applications for church office
|
||||
ChurchApplication.belongsTo(ChurchOfficeType, {
|
||||
foreignKey: 'officeTypeId',
|
||||
as: 'officeType'
|
||||
});
|
||||
ChurchOfficeType.hasMany(ChurchApplication, {
|
||||
foreignKey: 'officeTypeId',
|
||||
as: 'applications'
|
||||
});
|
||||
|
||||
ChurchApplication.belongsTo(FalukantCharacter, {
|
||||
foreignKey: 'characterId',
|
||||
as: 'applicant'
|
||||
});
|
||||
FalukantCharacter.hasMany(ChurchApplication, {
|
||||
foreignKey: 'characterId',
|
||||
as: 'churchApplications'
|
||||
});
|
||||
|
||||
ChurchApplication.belongsTo(FalukantCharacter, {
|
||||
foreignKey: 'supervisorId',
|
||||
as: 'supervisor'
|
||||
});
|
||||
FalukantCharacter.hasMany(ChurchApplication, {
|
||||
foreignKey: 'supervisorId',
|
||||
as: 'supervisedApplications'
|
||||
});
|
||||
|
||||
ChurchApplication.belongsTo(RegionData, {
|
||||
foreignKey: 'regionId',
|
||||
as: 'region'
|
||||
});
|
||||
RegionData.hasMany(ChurchApplication, {
|
||||
foreignKey: 'regionId',
|
||||
as: 'churchApplications'
|
||||
});
|
||||
|
||||
Underground.belongsTo(UndergroundType, {
|
||||
foreignKey: 'undergroundTypeId',
|
||||
as: 'undergroundType'
|
||||
@@ -941,5 +1055,41 @@ export default function setupAssociations() {
|
||||
|
||||
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
|
||||
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' });
|
||||
}
|
||||
|
||||
|
||||
86
backend/models/community/calendar_event.js
Normal file
86
backend/models/community/calendar_event.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class CalendarEvent extends Model { }
|
||||
|
||||
CalendarEvent.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'user',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
categoryId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: 'personal',
|
||||
comment: 'Category key: personal, work, family, health, birthday, holiday, reminder, other'
|
||||
},
|
||||
startDate: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false
|
||||
},
|
||||
endDate: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: true,
|
||||
comment: 'End date for multi-day events, null means same as startDate'
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: true,
|
||||
comment: 'Start time, null for all-day events'
|
||||
},
|
||||
endTime: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: true,
|
||||
comment: 'End time, null for all-day events'
|
||||
},
|
||||
allDay: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'CalendarEvent',
|
||||
tableName: 'calendar_event',
|
||||
schema: 'community',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
},
|
||||
{
|
||||
fields: ['user_id', 'start_date']
|
||||
},
|
||||
{
|
||||
fields: ['user_id', 'start_date', 'end_date']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default CalendarEvent;
|
||||
24
backend/models/community/user_dashboard.js
Normal file
24
backend/models/community/user_dashboard.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
import User from './user.js';
|
||||
|
||||
const UserDashboard = sequelize.define('user_dashboard', {
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
primaryKey: true,
|
||||
references: { model: User, key: 'id' }
|
||||
},
|
||||
config: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
defaultValue: { widgets: [] }
|
||||
}
|
||||
}, {
|
||||
tableName: 'user_dashboard',
|
||||
schema: 'community',
|
||||
underscored: true,
|
||||
timestamps: false
|
||||
});
|
||||
|
||||
export default UserDashboard;
|
||||
75
backend/models/community/vocab_course.js
Normal file
75
backend/models/community/vocab_course.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class VocabCourse extends Model {}
|
||||
|
||||
VocabCourse.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
ownerUserId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'owner_user_id'
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
languageId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'language_id'
|
||||
},
|
||||
nativeLanguageId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'native_language_id',
|
||||
comment: 'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".'
|
||||
},
|
||||
difficultyLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
field: 'difficulty_level'
|
||||
},
|
||||
isPublic: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'is_public'
|
||||
},
|
||||
shareCode: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
field: 'share_code'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'updated_at'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'VocabCourse',
|
||||
tableName: 'vocab_course',
|
||||
schema: 'community',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default VocabCourse;
|
||||
37
backend/models/community/vocab_course_enrollment.js
Normal file
37
backend/models/community/vocab_course_enrollment.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class VocabCourseEnrollment extends Model {}
|
||||
|
||||
VocabCourseEnrollment.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'user_id'
|
||||
},
|
||||
courseId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'course_id'
|
||||
},
|
||||
enrolledAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'enrolled_at'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'VocabCourseEnrollment',
|
||||
tableName: 'vocab_course_enrollment',
|
||||
schema: 'community',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default VocabCourseEnrollment;
|
||||
93
backend/models/community/vocab_course_lesson.js
Normal file
93
backend/models/community/vocab_course_lesson.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class VocabCourseLesson extends Model {}
|
||||
|
||||
VocabCourseLesson.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
courseId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'course_id'
|
||||
},
|
||||
chapterId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'chapter_id'
|
||||
},
|
||||
lessonNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'lesson_number'
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
weekNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'week_number'
|
||||
},
|
||||
dayNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'day_number'
|
||||
},
|
||||
lessonType: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
defaultValue: 'vocab',
|
||||
field: 'lesson_type'
|
||||
},
|
||||
audioUrl: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'audio_url'
|
||||
},
|
||||
culturalNotes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'cultural_notes'
|
||||
},
|
||||
targetMinutes: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'target_minutes'
|
||||
},
|
||||
targetScorePercent: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 80,
|
||||
field: 'target_score_percent'
|
||||
},
|
||||
requiresReview: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'requires_review'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'VocabCourseLesson',
|
||||
tableName: 'vocab_course_lesson',
|
||||
schema: 'community',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default VocabCourseLesson;
|
||||
56
backend/models/community/vocab_course_progress.js
Normal file
56
backend/models/community/vocab_course_progress.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class VocabCourseProgress extends Model {}
|
||||
|
||||
VocabCourseProgress.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'user_id'
|
||||
},
|
||||
courseId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'course_id'
|
||||
},
|
||||
lessonId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'lesson_id'
|
||||
},
|
||||
completed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
score: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
lastAccessedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'last_accessed_at'
|
||||
},
|
||||
completedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'completed_at'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'VocabCourseProgress',
|
||||
tableName: 'vocab_course_progress',
|
||||
schema: 'community',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default VocabCourseProgress;
|
||||
69
backend/models/community/vocab_grammar_exercise.js
Normal file
69
backend/models/community/vocab_grammar_exercise.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class VocabGrammarExercise extends Model {}
|
||||
|
||||
VocabGrammarExercise.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
lessonId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'lesson_id'
|
||||
},
|
||||
exerciseTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'exercise_type_id'
|
||||
},
|
||||
exerciseNumber: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'exercise_number'
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
instruction: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
questionData: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
field: 'question_data'
|
||||
},
|
||||
answerData: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
field: 'answer_data'
|
||||
},
|
||||
explanation: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
createdByUserId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'created_by_user_id'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'VocabGrammarExercise',
|
||||
tableName: 'vocab_grammar_exercise',
|
||||
schema: 'community',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default VocabGrammarExercise;
|
||||
57
backend/models/community/vocab_grammar_exercise_progress.js
Normal file
57
backend/models/community/vocab_grammar_exercise_progress.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class VocabGrammarExerciseProgress extends Model {}
|
||||
|
||||
VocabGrammarExerciseProgress.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'user_id'
|
||||
},
|
||||
exerciseId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'exercise_id'
|
||||
},
|
||||
attempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
correctAttempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'correct_attempts'
|
||||
},
|
||||
lastAttemptAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'last_attempt_at'
|
||||
},
|
||||
completed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
completedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'completed_at'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'VocabGrammarExerciseProgress',
|
||||
tableName: 'vocab_grammar_exercise_progress',
|
||||
schema: 'community',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default VocabGrammarExerciseProgress;
|
||||
36
backend/models/community/vocab_grammar_exercise_type.js
Normal file
36
backend/models/community/vocab_grammar_exercise_type.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class VocabGrammarExerciseType extends Model {}
|
||||
|
||||
VocabGrammarExerciseType.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'VocabGrammarExerciseType',
|
||||
tableName: 'vocab_grammar_exercise_type',
|
||||
schema: 'community',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default VocabGrammarExerciseType;
|
||||
47
backend/models/falukant/data/church_application.js
Normal file
47
backend/models/falukant/data/church_application.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class ChurchApplication extends Model {}
|
||||
|
||||
ChurchApplication.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
officeTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
characterId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
regionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
supervisorId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'ID des Vorgesetzten, der über die Bewerbung entscheidet (null für Einstiegspositionen ohne Supervisor)'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'approved', 'rejected'),
|
||||
allowNull: false,
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
decisionDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ChurchApplication',
|
||||
tableName: 'church_application',
|
||||
schema: 'falukant_data',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default ChurchApplication;
|
||||
38
backend/models/falukant/data/church_office.js
Normal file
38
backend/models/falukant/data/church_office.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class ChurchOffice extends Model {}
|
||||
|
||||
ChurchOffice.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
officeTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
characterId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
regionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
supervisorId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'ID des Vorgesetzten (höhere Position in der Hierarchie)'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ChurchOffice',
|
||||
tableName: 'church_office',
|
||||
schema: 'falukant_data',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default ChurchOffice;
|
||||
@@ -22,7 +22,12 @@ Production.init({
|
||||
startTimestamp: {
|
||||
type: DataTypes.DATE,
|
||||
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,
|
||||
modelName: 'Production',
|
||||
|
||||
@@ -10,11 +10,20 @@ RegionData.init({
|
||||
allowNull: false},
|
||||
regionTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: RegionType,
|
||||
key: 'id',
|
||||
schema: 'falukant_type'
|
||||
}
|
||||
},
|
||||
parentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'region',
|
||||
key: 'id',
|
||||
schema: 'falukant_data'}
|
||||
},
|
||||
map: {
|
||||
type: DataTypes.JSONB,
|
||||
|
||||
@@ -6,7 +6,8 @@ class FalukantStock extends Model { }
|
||||
FalukantStock.init({
|
||||
branchId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
stockTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
44
backend/models/falukant/log/product_price_history.js
Normal file
44
backend/models/falukant/log/product_price_history.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
/**
|
||||
* Preishistorie pro Produkt und Region (Zeitreihe für Preis-Graphen).
|
||||
* Aktuell wird diese Tabelle noch nicht befüllt; sie dient nur als Grundlage.
|
||||
*/
|
||||
class ProductPriceHistory extends Model { }
|
||||
|
||||
ProductPriceHistory.init({
|
||||
productId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
regionId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
price: {
|
||||
type: DataTypes.DECIMAL(12, 2),
|
||||
allowNull: false
|
||||
},
|
||||
recordedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ProductPriceHistory',
|
||||
tableName: 'product_price_history',
|
||||
schema: 'falukant_log',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
name: 'product_price_history_product_region_recorded_idx',
|
||||
fields: ['product_id', 'region_id', 'recorded_at']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default ProductPriceHistory;
|
||||
|
||||
49
backend/models/falukant/log/relationship_change_log.js
Normal file
49
backend/models/falukant/log/relationship_change_log.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
/**
|
||||
* Log aller Änderungen an relationship und marriage_proposals.
|
||||
* Einträge werden ausschließlich durch DB-Trigger geschrieben und nicht gelöscht.
|
||||
* Hilft zu analysieren, warum z.B. Werbungen um einen Partner verschwinden.
|
||||
*/
|
||||
class RelationshipChangeLog extends Model {}
|
||||
|
||||
RelationshipChangeLog.init(
|
||||
{
|
||||
changedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
tableName: {
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: false
|
||||
},
|
||||
operation: {
|
||||
type: DataTypes.STRING(16),
|
||||
allowNull: false
|
||||
},
|
||||
recordId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
payloadOld: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
payloadNew: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'RelationshipChangeLog',
|
||||
tableName: 'relationship_change_log',
|
||||
schema: 'falukant_log',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
}
|
||||
);
|
||||
|
||||
export default RelationshipChangeLog;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class ChurchOfficeRequirement extends Model {}
|
||||
|
||||
ChurchOfficeRequirement.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
officeTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
prerequisiteOfficeTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Erforderliche niedrigere Position in der Hierarchie'
|
||||
},
|
||||
minTitleLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Mindest-Titel-Level (optional)'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ChurchOfficeRequirement',
|
||||
tableName: 'church_office_requirement',
|
||||
schema: 'falukant_predefine',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default ChurchOfficeRequirement;
|
||||
@@ -10,12 +10,14 @@ PromotionalGiftCharacterTrait.init(
|
||||
giftId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'gift_id',
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
traitId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'trait_id',
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
suitability: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
@@ -10,12 +10,14 @@ PromotionalGiftMood.init(
|
||||
giftId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'gift_id',
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
moodId: {
|
||||
type: DataTypes.INTEGER,
|
||||
field: 'mood_id',
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
suitability: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
38
backend/models/falukant/type/church_office_type.js
Normal file
38
backend/models/falukant/type/church_office_type.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class ChurchOfficeType extends Model {}
|
||||
|
||||
ChurchOfficeType.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
seatsPerRegion: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
regionType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
hierarchyLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: 'Höhere Zahl = höhere Position in der Hierarchie'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ChurchOfficeType',
|
||||
tableName: 'church_office_type',
|
||||
schema: 'falukant_type',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default ChurchOfficeType;
|
||||
@@ -15,7 +15,8 @@ ProductType.init({
|
||||
allowNull: false},
|
||||
sellCost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false}
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'ProductType',
|
||||
|
||||
@@ -4,8 +4,10 @@ import SettingsType from './type/settings.js';
|
||||
import UserParamValue from './type/user_param_value.js';
|
||||
import UserParamType from './type/user_param.js';
|
||||
import UserRightType from './type/user_right.js';
|
||||
import WidgetType from './type/widget_type.js';
|
||||
import User from './community/user.js';
|
||||
import UserParam from './community/user_param.js';
|
||||
import UserDashboard from './community/user_dashboard.js';
|
||||
import Login from './logs/login.js';
|
||||
import UserRight from './community/user_right.js';
|
||||
import InterestType from './type/interest.js';
|
||||
@@ -87,6 +89,7 @@ import Learning from './falukant/data/learning.js';
|
||||
import Credit from './falukant/data/credit.js';
|
||||
import DebtorsPrism from './falukant/data/debtors_prism.js';
|
||||
import HealthActivity from './falukant/log/health_activity.js';
|
||||
import ProductPriceHistory from './falukant/log/product_price_history.js';
|
||||
|
||||
// — Match3 Minigame —
|
||||
import Match3Campaign from './match3/campaign.js';
|
||||
@@ -113,6 +116,13 @@ import Vote from './falukant/data/vote.js';
|
||||
import ElectionResult from './falukant/data/election_result.js';
|
||||
import PoliticalOfficeHistory from './falukant/log/political_office_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 Underground from './falukant/data/underground.js';
|
||||
import VehicleType from './falukant/type/vehicle.js';
|
||||
@@ -129,13 +139,25 @@ import ChatRight from './chat/rights.js';
|
||||
import ChatUserRight from './chat/user_rights.js';
|
||||
import 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 = {
|
||||
SettingsType,
|
||||
UserParamValue,
|
||||
UserParamType,
|
||||
UserRightType,
|
||||
WidgetType,
|
||||
User,
|
||||
UserParam,
|
||||
UserDashboard,
|
||||
Login,
|
||||
UserRight,
|
||||
InterestType,
|
||||
@@ -218,6 +240,7 @@ const models = {
|
||||
Credit,
|
||||
DebtorsPrism,
|
||||
HealthActivity,
|
||||
ProductPriceHistory,
|
||||
RegionDistance,
|
||||
VehicleType,
|
||||
Vehicle,
|
||||
@@ -233,6 +256,11 @@ const models = {
|
||||
ElectionResult,
|
||||
PoliticalOfficeHistory,
|
||||
ElectionHistory,
|
||||
RelationshipChangeLog,
|
||||
ChurchOfficeType,
|
||||
ChurchOfficeRequirement,
|
||||
ChurchOffice,
|
||||
ChurchApplication,
|
||||
UndergroundType,
|
||||
Underground,
|
||||
WeatherType,
|
||||
@@ -263,6 +291,18 @@ const models = {
|
||||
TaxiMapTileStreet,
|
||||
TaxiMapTileHouse,
|
||||
TaxiHighscore,
|
||||
|
||||
// Vocab Courses
|
||||
VocabCourse,
|
||||
VocabCourseLesson,
|
||||
VocabCourseEnrollment,
|
||||
VocabCourseProgress,
|
||||
VocabGrammarExerciseType,
|
||||
VocabGrammarExercise,
|
||||
VocabGrammarExerciseProgress,
|
||||
|
||||
// Calendar
|
||||
CalendarEvent,
|
||||
};
|
||||
|
||||
export default models;
|
||||
|
||||
@@ -350,15 +350,16 @@ export async function createTriggers() {
|
||||
SELECT * FROM random_fill
|
||||
),
|
||||
|
||||
-- 8) Neue Ämter anlegen und sofort zurückliefern
|
||||
-- 8) Neue Ämter anlegen – created_at = Wahldatum (Amtsbeginn), nicht NOW()
|
||||
-- damit termEnds = Amtsbeginn + termLength korrekt berechnet werden kann
|
||||
created_offices AS (
|
||||
INSERT INTO falukant_data.political_office
|
||||
(office_type_id, character_id, created_at, updated_at, region_id)
|
||||
SELECT
|
||||
tp.tp_office_type_id,
|
||||
fw.character_id,
|
||||
NOW() AS created_at,
|
||||
NOW() AS updated_at,
|
||||
tp.tp_election_date AS created_at,
|
||||
tp.tp_election_date AS updated_at,
|
||||
tp.tp_region_id
|
||||
FROM final_winners fw
|
||||
JOIN to_process tp
|
||||
|
||||
39
backend/models/type/widget_type.js
Normal file
39
backend/models/type/widget_type.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const WidgetType = sequelize.define('widget_type', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
label: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: 'Anzeigename des Widgets (z. B. "Termine")'
|
||||
},
|
||||
endpoint: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: 'API-Pfad (z. B. "/api/termine")'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: 'Optionale Beschreibung'
|
||||
},
|
||||
orderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'order_id',
|
||||
comment: 'Sortierreihenfolge'
|
||||
}
|
||||
}, {
|
||||
tableName: 'widget_type',
|
||||
schema: 'type',
|
||||
underscored: true,
|
||||
timestamps: false
|
||||
});
|
||||
|
||||
export default WidgetType;
|
||||
4322
backend/package-lock.json
generated
4322
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,9 @@
|
||||
"dev": "NODE_ENV=development node server.js",
|
||||
"start-daemon": "node daemonServer.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"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -37,7 +40,8 @@
|
||||
"sharp": "^0.34.3",
|
||||
"socket.io": "^4.7.5",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.0"
|
||||
"ws": "^8.18.0",
|
||||
"@gltf-transform/cli": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sequelize-cli": "^6.6.2"
|
||||
|
||||
20
backend/routers/calendarRouter.js
Normal file
20
backend/routers/calendarRouter.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import calendarController from '../controllers/calendarController.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.get('/events', authenticate, calendarController.getEvents);
|
||||
router.get('/events/:id', authenticate, calendarController.getEvent);
|
||||
router.post('/events', authenticate, calendarController.createEvent);
|
||||
router.put('/events/:id', authenticate, calendarController.updateEvent);
|
||||
router.delete('/events/:id', authenticate, calendarController.deleteEvent);
|
||||
router.get('/birthdays', authenticate, calendarController.getFriendsBirthdays);
|
||||
|
||||
// Widget endpoints
|
||||
router.get('/widget/birthdays', authenticate, calendarController.getWidgetBirthdays);
|
||||
router.get('/widget/upcoming', authenticate, calendarController.getWidgetUpcoming);
|
||||
router.get('/widget/mini', authenticate, calendarController.getWidgetMiniCalendar);
|
||||
|
||||
export default router;
|
||||
11
backend/routers/dashboardRouter.js
Normal file
11
backend/routers/dashboardRouter.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import dashboardController from '../controllers/dashboardController.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/widgets', authenticate, dashboardController.getAvailableWidgets.bind(dashboardController));
|
||||
router.get('/config', authenticate, dashboardController.getConfig.bind(dashboardController));
|
||||
router.put('/config', authenticate, dashboardController.setConfig.bind(dashboardController));
|
||||
|
||||
export default router;
|
||||
@@ -11,6 +11,7 @@ router.get('/character/affect', falukantController.getCharacterAffect);
|
||||
router.get('/name/randomfirstname/:gender', falukantController.randomFirstName);
|
||||
router.get('/name/randomlastname', falukantController.randomLastName);
|
||||
router.get('/info', falukantController.getInfo);
|
||||
router.get('/dashboard-widget', falukantController.getDashboardWidget);
|
||||
router.get('/branches/types', falukantController.getBranchTypes);
|
||||
router.get('/branches/:branch', falukantController.getBranch);
|
||||
router.get('/branches', falukantController.getBranches);
|
||||
@@ -28,6 +29,7 @@ router.get('/inventory/?:branchId', falukantController.getInventory);
|
||||
router.post('/sell/all', falukantController.sellAllProducts);
|
||||
router.post('/sell', falukantController.sellProduct);
|
||||
router.post('/moneyhistory', falukantController.moneyHistory);
|
||||
router.post('/moneyhistory/graph', falukantController.moneyHistoryGraph);
|
||||
router.get('/storage/:branchId', falukantController.getStorage);
|
||||
router.post('/storage', falukantController.buyStorage);
|
||||
router.delete('/storage', falukantController.sellStorage);
|
||||
@@ -38,14 +40,14 @@ router.get('/director/:branchId', falukantController.getDirectorForBranch);
|
||||
router.get('/directors', falukantController.getAllDirectors);
|
||||
router.post('/directors', falukantController.updateDirector);
|
||||
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
|
||||
router.post('/family/cancel-wooing', falukantController.cancelWooing);
|
||||
router.post('/family/set-heir', falukantController.setHeir);
|
||||
router.get('/heirs/potential', falukantController.getPotentialHeirs);
|
||||
router.post('/heirs/select', falukantController.selectHeir);
|
||||
router.get('/family/gifts', falukantController.getGifts);
|
||||
router.get('/family/children', falukantController.getChildren);
|
||||
router.post('/family/gift', falukantController.sendGift);
|
||||
router.get('/family', falukantController.getFamily);
|
||||
router.get('/nobility/titels', falukantController.getTitlesOfNobility);
|
||||
router.get('/reputation/actions', falukantController.getReputationActions);
|
||||
router.get('/houses/types', falukantController.getHouseTypes);
|
||||
router.get('/houses/buyable', falukantController.getBuyableHouses);
|
||||
router.get('/houses', falukantController.getUserHouse);
|
||||
@@ -55,10 +57,13 @@ router.post('/houses', falukantController.buyUserHouse);
|
||||
router.get('/party/types', falukantController.getPartyTypes);
|
||||
router.post('/party', falukantController.createParty);
|
||||
router.get('/party', falukantController.getParties);
|
||||
router.get('/reputation/actions', falukantController.getReputationActions);
|
||||
router.post('/reputation/actions', falukantController.executeReputationAction);
|
||||
router.get('/family/notbaptised', falukantController.getNotBaptisedChildren);
|
||||
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.post('/education', falukantController.sendToSchool);
|
||||
router.get('/bank/overview', falukantController.getBankOverview);
|
||||
@@ -76,7 +81,9 @@ router.get('/politics/open', falukantController.getOpenPolitics);
|
||||
router.post('/politics/open', falukantController.applyForElections);
|
||||
router.get('/cities', falukantController.getRegions);
|
||||
router.get('/products/price-in-region', falukantController.getProductPriceInRegion);
|
||||
router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion);
|
||||
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('/vehicles/types', falukantController.getVehicleTypes);
|
||||
router.post('/vehicles', falukantController.buyVehicles);
|
||||
|
||||
28
backend/routers/modelsProxyRouter.js
Normal file
28
backend/routers/modelsProxyRouter.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { getOptimizedModelPath } from '../services/modelsProxyService.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/models/3d/falukant/characters/:filename
|
||||
* Liefert die Draco-optimierte GLB-Datei (aus Cache oder nach Optimierung).
|
||||
*/
|
||||
router.get('/3d/falukant/characters/:filename', async (req, res) => {
|
||||
const { filename } = req.params;
|
||||
|
||||
try {
|
||||
const cachePath = await getOptimizedModelPath(filename);
|
||||
res.setHeader('Content-Type', 'model/gltf-binary');
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
res.sendFile(cachePath);
|
||||
} catch (e) {
|
||||
if (e.message?.includes('Invalid model filename') || e.message?.includes('not found')) {
|
||||
return res.status(404).send(e.message);
|
||||
}
|
||||
console.error('[models-proxy]', e.message);
|
||||
res.status(500).send('Model optimization failed');
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
9
backend/routers/newsRouter.js
Normal file
9
backend/routers/newsRouter.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import newsController from '../controllers/newsController.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, newsController.getNews.bind(newsController));
|
||||
|
||||
export default router;
|
||||
@@ -8,6 +8,7 @@ const vocabController = new VocabController();
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/languages', vocabController.listLanguages);
|
||||
router.get('/languages/all', vocabController.listAllLanguages);
|
||||
router.post('/languages', vocabController.createLanguage);
|
||||
router.post('/subscribe', vocabController.subscribe);
|
||||
router.get('/languages/:languageId', vocabController.getLanguage);
|
||||
@@ -22,6 +23,39 @@ router.get('/chapters/:chapterId', vocabController.getChapter);
|
||||
router.get('/chapters/:chapterId/vocabs', vocabController.listChapterVocabs);
|
||||
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;
|
||||
|
||||
|
||||
|
||||
116
backend/scripts/add-bisaya-week1-lessons.js
Normal file
116
backend/scripts/add-bisaya-week1-lessons.js
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Hinzufügen der Lektionen 9 und 10 (Woche 1 - Wiederholung, Woche 1 - Vokabeltest)
|
||||
* zu bestehenden Bisaya-Kursen, falls diese noch fehlen.
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/add-bisaya-week1-lessons.js
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
|
||||
const LESSONS_TO_ADD = [
|
||||
{
|
||||
lessonNumber: 9,
|
||||
weekNumber: 1,
|
||||
dayNumber: 5,
|
||||
lessonType: 'review',
|
||||
title: 'Woche 1 - Wiederholung',
|
||||
description: 'Wiederhole alle Inhalte der ersten Woche',
|
||||
culturalNotes: 'Wiederholung ist der Schlüssel zum Erfolg!',
|
||||
targetMinutes: 30,
|
||||
targetScorePercent: 80,
|
||||
requiresReview: false
|
||||
},
|
||||
{
|
||||
lessonNumber: 10,
|
||||
weekNumber: 1,
|
||||
dayNumber: 5,
|
||||
lessonType: 'vocab',
|
||||
title: 'Woche 1 - Vokabeltest',
|
||||
description: 'Teste dein Wissen aus Woche 1',
|
||||
culturalNotes: null,
|
||||
targetMinutes: 15,
|
||||
targetScorePercent: 80,
|
||||
requiresReview: true
|
||||
}
|
||||
];
|
||||
|
||||
async function addBisayaWeek1Lessons() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (!bisayaLanguage) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const courses = await sequelize.query(
|
||||
`SELECT id, title FROM community.vocab_course WHERE language_id = :languageId`,
|
||||
{
|
||||
replacements: { languageId: bisayaLanguage.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${courses.length} Bisaya-Kurs(e)\n`);
|
||||
|
||||
let totalAdded = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
|
||||
for (const lessonData of LESSONS_TO_ADD) {
|
||||
const existing = await VocabCourseLesson.findOne({
|
||||
where: {
|
||||
courseId: course.id,
|
||||
lessonNumber: lessonData.lessonNumber
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(` ⏭️ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" - bereits vorhanden`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await VocabCourseLesson.create({
|
||||
courseId: course.id,
|
||||
chapterId: null,
|
||||
lessonNumber: lessonData.lessonNumber,
|
||||
title: lessonData.title,
|
||||
description: lessonData.description,
|
||||
weekNumber: lessonData.weekNumber,
|
||||
dayNumber: lessonData.dayNumber,
|
||||
lessonType: lessonData.lessonType,
|
||||
culturalNotes: lessonData.culturalNotes,
|
||||
targetMinutes: lessonData.targetMinutes,
|
||||
targetScorePercent: lessonData.targetScorePercent,
|
||||
requiresReview: lessonData.requiresReview
|
||||
});
|
||||
|
||||
console.log(` ✅ Lektion ${lessonData.lessonNumber}: "${lessonData.title}" hinzugefügt`);
|
||||
totalAdded++;
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Fertig! ${totalAdded} Lektion(en) hinzugefügt.`);
|
||||
console.log('💡 Führe danach create-bisaya-course-content.js aus, um die Übungen zu erstellen.');
|
||||
}
|
||||
|
||||
addBisayaWeek1Lessons()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
141
backend/scripts/add-grammar-exercises-to-existing-courses.js
Normal file
141
backend/scripts/add-grammar-exercises-to-existing-courses.js
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Hinzufügen von Grammatik-Übungen zu bestehenden Kursen
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/add-grammar-exercises-to-existing-courses.js
|
||||
*
|
||||
* Fügt Beispiel-Grammatik-Übungen zu allen Grammar-Lektionen hinzu, die noch keine Übungen haben.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import User from '../models/community/user.js';
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'system'
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'admin'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!systemUser) {
|
||||
console.error('❌ System-Benutzer nicht gefunden. Bitte erstelle einen System-Benutzer.');
|
||||
throw new Error('System user not found');
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
// Erstelle Beispiel-Grammatik-Übungen für eine Grammar-Lektion
|
||||
function createExampleGrammarExercises(lessonId, lessonTitle, ownerUserId) {
|
||||
const exercises = [];
|
||||
|
||||
// Beispiel-Übung 1: Gap Fill (Lückentext)
|
||||
exercises.push({
|
||||
lessonId: lessonId,
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
exerciseNumber: 1,
|
||||
title: `${lessonTitle} - Übung 1`,
|
||||
instruction: 'Fülle die Lücken mit den richtigen Wörtern.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
text: 'Hallo! Wie geht es {gap}? Mir geht es {gap}, danke!',
|
||||
gaps: 2
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
answers: ['dir', 'gut']
|
||||
}),
|
||||
explanation: 'Die richtigen Antworten sind "dir" und "gut".',
|
||||
createdByUserId: ownerUserId
|
||||
});
|
||||
|
||||
// Beispiel-Übung 2: Multiple Choice
|
||||
exercises.push({
|
||||
lessonId: lessonId,
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: 2,
|
||||
title: `${lessonTitle} - Übung 2`,
|
||||
instruction: 'Wähle die richtige Antwort aus.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Guten Tag"?',
|
||||
options: ['Guten Tag', 'Gute Nacht', 'Auf Wiedersehen', 'Tschüss']
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
}),
|
||||
explanation: 'Die richtige Antwort ist "Guten Tag".',
|
||||
createdByUserId: ownerUserId
|
||||
});
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
async function addGrammarExercisesToExistingCourses() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
|
||||
|
||||
// Finde alle Grammar-Lektionen ohne Übungen
|
||||
const grammarLessons = await sequelize.query(
|
||||
`SELECT l.id, l.title, l.course_id, c.owner_user_id
|
||||
FROM community.vocab_course_lesson l
|
||||
JOIN community.vocab_course c ON c.id = l.course_id
|
||||
WHERE l.lesson_type = 'grammar'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM community.vocab_grammar_exercise e
|
||||
WHERE e.lesson_id = l.id
|
||||
)
|
||||
ORDER BY l.course_id, l.lesson_number`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${grammarLessons.length} Grammar-Lektionen ohne Übungen\n`);
|
||||
|
||||
if (grammarLessons.length === 0) {
|
||||
console.log('✅ Alle Grammar-Lektionen haben bereits Übungen.');
|
||||
return;
|
||||
}
|
||||
|
||||
let addedCount = 0;
|
||||
for (const lesson of grammarLessons) {
|
||||
const exercises = createExampleGrammarExercises(lesson.id, lesson.title, lesson.owner_user_id);
|
||||
|
||||
for (const exercise of exercises) {
|
||||
await VocabGrammarExercise.create(exercise);
|
||||
addedCount++;
|
||||
}
|
||||
|
||||
console.log(`✅ ${exercises.length} Übungen zu "${lesson.title}" hinzugefügt`);
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${addedCount} Grammatik-Übungen zu ${grammarLessons.length} Lektionen hinzugefügt`);
|
||||
}
|
||||
|
||||
addGrammarExercisesToExistingCourses()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
829
backend/scripts/create-bisaya-course-content.js
Normal file
829
backend/scripts/create-bisaya-course-content.js
Normal file
@@ -0,0 +1,829 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Erstellen von sprachspezifischem Content für Bisaya-Kurse
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/create-bisaya-course-content.js
|
||||
*
|
||||
* Erstellt Grammatik-Übungen für alle Lektionen in Bisaya-Kursen basierend auf dem Thema der Lektion.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import User from '../models/community/user.js';
|
||||
|
||||
// Bisaya-spezifische Übungen basierend auf Lektionsthemen
|
||||
const BISAYA_EXERCISES = {
|
||||
// Lektion 1: Begrüßungen & Höflichkeit
|
||||
'Begrüßungen & Höflichkeit': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Wie geht es dir?" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Begrüßung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wie geht es dir?" auf Bisaya?',
|
||||
options: ['Kumusta ka?', 'Maayo', 'Salamat', 'Palihug']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Kumusta ka?" ist die Standard-Begrüßung auf Bisaya, ähnlich wie "Wie geht es dir?"'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Begrüßungen vervollständigen',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Wörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: 'Kumusta ka? Maayo {gap} (ich). Salamat.',
|
||||
gaps: 1
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['ko']
|
||||
},
|
||||
explanation: '"Maayo ko" bedeutet "Mir geht es gut". "ko" ist "ich" auf Bisaya.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Was bedeutet "Salamat"?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Salamat"?',
|
||||
options: ['Danke', 'Bitte', 'Entschuldigung', 'Auf Wiedersehen']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Salamat" bedeutet "Danke" auf Bisaya.'
|
||||
}
|
||||
],
|
||||
|
||||
// Lektion 3: Familienwörter
|
||||
'Familienwörter': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Mutter" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Mutter" auf Bisaya?',
|
||||
options: ['Nanay', 'Tatay', 'Kuya', 'Ate']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Nanay" bedeutet "Mutter" auf Bisaya. "Mama" wird auch verwendet.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Vater" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Vater" auf Bisaya?',
|
||||
options: ['Tatay', 'Nanay', 'Kuya', 'Ate']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Tatay" bedeutet "Vater" auf Bisaya. "Papa" wird auch verwendet.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "älterer Bruder" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "älterer Bruder" auf Bisaya?',
|
||||
options: ['Kuya', 'Ate', 'Nanay', 'Tatay']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Kuya" bedeutet "älterer Bruder" auf Bisaya. Wird auch für respektvolle Anrede von älteren Männern verwendet.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "ältere Schwester" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "ältere Schwester" auf Bisaya?',
|
||||
options: ['Ate', 'Kuya', 'Nanay', 'Tatay']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Ate" bedeutet "ältere Schwester" auf Bisaya. Wird auch für respektvolle Anrede von älteren Frauen verwendet.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Großmutter" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Großmutter" auf Bisaya?',
|
||||
options: ['Lola', 'Lolo', 'Nanay', 'Tatay']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Lola" bedeutet "Großmutter" auf Bisaya.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Großvater" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Großvater" auf Bisaya?',
|
||||
options: ['Lolo', 'Lola', 'Nanay', 'Tatay']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Lolo" bedeutet "Großvater" auf Bisaya.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Familienwörter vervollständigen',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Familienwörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap} (Mutter) | {gap} (Vater) | {gap} (älterer Bruder) | {gap} (ältere Schwester) | {gap} (Großmutter) | {gap} (Großvater)',
|
||||
gaps: 6
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Nanay', 'Tatay', 'Kuya', 'Ate', 'Lola', 'Lolo']
|
||||
},
|
||||
explanation: 'Nanay = Mutter, Tatay = Vater, Kuya = älterer Bruder, Ate = ältere Schwester, Lola = Großmutter, Lolo = Großvater.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Familienwörter übersetzen',
|
||||
instruction: 'Übersetze das Familienwort ins Bisaya.',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Mutter',
|
||||
sourceLanguage: 'Deutsch',
|
||||
targetLanguage: 'Bisaya'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Nanay',
|
||||
alternatives: ['Mama', 'Nanay', 'Inahan']
|
||||
},
|
||||
explanation: '"Nanay" oder "Mama" bedeutet "Mutter" auf Bisaya.'
|
||||
}
|
||||
],
|
||||
|
||||
// Lektion 15: Zeitformen - Grundlagen
|
||||
'Zeitformen - Grundlagen': [
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Vergangenheit verstehen',
|
||||
instruction: 'Was bedeutet "Ni-kaon ko"?',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Ni-kaon ko',
|
||||
sourceLanguage: 'Bisaya',
|
||||
targetLanguage: 'Deutsch'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Ich habe gegessen',
|
||||
alternatives: ['I ate', 'I have eaten']
|
||||
},
|
||||
explanation: 'Das Präfix "Ni-" zeigt die Vergangenheit an. "Ni-kaon ko" bedeutet "Ich habe gegessen".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Zukunft verstehen',
|
||||
instruction: 'Was bedeutet "Mo-kaon ko"?',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Mo-kaon ko',
|
||||
sourceLanguage: 'Bisaya',
|
||||
targetLanguage: 'Deutsch'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Ich werde essen',
|
||||
alternatives: ['I will eat', 'I am going to eat']
|
||||
},
|
||||
explanation: 'Das Präfix "Mo-" zeigt die Zukunft an. "Mo-kaon ko" bedeutet "Ich werde essen".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Zeitformen erkennen',
|
||||
instruction: 'Setze die richtigen Präfixe ein.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap}-kaon ko (Vergangenheit) | {gap}-kaon ko (Zukunft)',
|
||||
gaps: 2
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Ni', 'Mo']
|
||||
},
|
||||
explanation: 'Ni- für Vergangenheit, Mo- für Zukunft.'
|
||||
}
|
||||
],
|
||||
|
||||
// Lektion 25: Höflichkeitsformen
|
||||
'Höflichkeitsformen': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Bitte langsam"?',
|
||||
instruction: 'Wähle die höfliche Form aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Bitte langsam" auf Bisaya?',
|
||||
options: ['Hinay-hinay lang', 'Palihug', 'Salamat', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Hinay-hinay lang" bedeutet "Bitte langsam" und ist sehr höflich.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Höfliche Sätze vervollständigen',
|
||||
instruction: 'Fülle die Lücken mit höflichen Wörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap} lang (Bitte langsam), wala ko kasabot. {gap} ka mubalik? (Bitte wiederholen)',
|
||||
gaps: 2
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Hinay-hinay', 'Palihug']
|
||||
},
|
||||
explanation: '"Hinay-hinay lang" = "Bitte langsam", "Palihug" = "Bitte".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Was bedeutet "Palihug"?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Palihug"?',
|
||||
options: ['Bitte', 'Danke', 'Entschuldigung', 'Gern geschehen']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Palihug" bedeutet "Bitte" auf Bisaya und wird für höfliche Bitten verwendet.'
|
||||
}
|
||||
],
|
||||
|
||||
// Lektion: Überlebenssätze
|
||||
'Überlebenssätze': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Ich verstehe nicht"?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
|
||||
options: ['Wala ko kasabot', 'Palihug', 'Salamat', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht" - sehr wichtig für Anfänger!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Kannst du das wiederholen?"?',
|
||||
instruction: 'Wähle die richtige Bitte aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Kannst du das wiederholen?" auf Bisaya?',
|
||||
options: ['Palihug ka mubalik?', 'Salamat', 'Maayo', 'Kumusta ka?']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?" - essentiell für das Lernen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Überlebenssätze vervollständigen',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Wörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap} ko kasabot (Ich verstehe nicht). {gap} ka mubalik? (Bitte wiederholen) {gap} lang (Bitte langsam).',
|
||||
gaps: 3
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Wala', 'Palihug', 'Hinay-hinay']
|
||||
},
|
||||
explanation: '"Wala ko kasabot" = "Ich verstehe nicht", "Palihug ka mubalik?" = "Bitte wiederholen", "Hinay-hinay lang" = "Bitte langsam".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Wo ist...?"?',
|
||||
instruction: 'Wähle die richtige Frage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wo ist die Toilette?" auf Bisaya?',
|
||||
options: ['Asa ang CR?', 'Kumusta ka?', 'Salamat', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Asa ang CR?" bedeutet "Wo ist die Toilette?" - "Asa" = "Wo", "CR" = "Comfort Room" (Toilette).'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Wie viel kostet das?"?',
|
||||
instruction: 'Wähle die richtige Frage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wie viel kostet das?" auf Bisaya?',
|
||||
options: ['Tagpila ni?', 'Asa ni?', 'Unsa ni?', 'Kinsa ni?']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Wichtige Fragen bilden',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Fragewörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap} ang CR? (Wo ist die Toilette?) | {gap} ni? (Wie viel kostet das?) | {gap} ni? (Was ist das?)',
|
||||
gaps: 3
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Asa', 'Tagpila', 'Unsa']
|
||||
},
|
||||
explanation: '"Asa" = "Wo", "Tagpila" = "Wie viel", "Unsa" = "Was".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Entschuldigung"?',
|
||||
instruction: 'Wähle die richtige Entschuldigung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Entschuldigung" auf Bisaya?',
|
||||
options: ['Pasensya', 'Salamat', 'Palihug', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Überlebenssätze übersetzen',
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Ich spreche kein Bisaya',
|
||||
sourceLanguage: 'Deutsch',
|
||||
targetLanguage: 'Bisaya'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Dili ko mag-Bisaya',
|
||||
alternatives: ['Wala ko mag-Bisaya', 'Dili ko makasabot Bisaya']
|
||||
},
|
||||
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
|
||||
}
|
||||
],
|
||||
|
||||
// Auch für "Überlebenssätze - Teil 1" und "Überlebenssätze - Teil 2"
|
||||
'Überlebenssätze - Teil 1': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Ich verstehe nicht"?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
|
||||
options: ['Wala ko kasabot', 'Palihug', 'Salamat', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht" - sehr wichtig für Anfänger!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Kannst du das wiederholen?"?',
|
||||
instruction: 'Wähle die richtige Bitte aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Kannst du das wiederholen?" auf Bisaya?',
|
||||
options: ['Palihug ka mubalik?', 'Salamat', 'Maayo', 'Kumusta ka?']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?" - essentiell für das Lernen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Wo ist...?"?',
|
||||
instruction: 'Wähle die richtige Frage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wo ist die Toilette?" auf Bisaya?',
|
||||
options: ['Asa ang CR?', 'Kumusta ka?', 'Salamat', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Asa ang CR?" bedeutet "Wo ist die Toilette?" - "Asa" = "Wo", "CR" = "Comfort Room" (Toilette).'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Überlebenssätze vervollständigen',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Bisaya-Wörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap} ko kasabot (Ich verstehe nicht). {gap} ka mubalik? (Bitte wiederholen)',
|
||||
gaps: 2
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Wala', 'Palihug']
|
||||
},
|
||||
explanation: '"Wala ko kasabot" = "Ich verstehe nicht", "Palihug ka mubalik?" = "Bitte wiederholen".'
|
||||
}
|
||||
],
|
||||
|
||||
'Überlebenssätze - Teil 2': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Wie viel kostet das?"?',
|
||||
instruction: 'Wähle die richtige Frage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wie viel kostet das?" auf Bisaya?',
|
||||
options: ['Tagpila ni?', 'Asa ni?', 'Unsa ni?', 'Kinsa ni?']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Entschuldigung"?',
|
||||
instruction: 'Wähle die richtige Entschuldigung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Entschuldigung" auf Bisaya?',
|
||||
options: ['Pasensya', 'Salamat', 'Palihug', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Wichtige Fragen bilden',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Fragewörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap} ni? (Wie viel kostet das?) | {gap} ni? (Was ist das?) | {gap} lang (Bitte langsam)',
|
||||
gaps: 3
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Tagpila', 'Unsa', 'Hinay-hinay']
|
||||
},
|
||||
explanation: '"Tagpila" = "Wie viel", "Unsa" = "Was", "Hinay-hinay lang" = "Bitte langsam".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Überlebenssätze übersetzen',
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Ich spreche kein Bisaya',
|
||||
sourceLanguage: 'Deutsch',
|
||||
targetLanguage: 'Bisaya'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Dili ko mag-Bisaya',
|
||||
alternatives: ['Wala ko mag-Bisaya', 'Dili ko makasabot Bisaya']
|
||||
},
|
||||
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
|
||||
}
|
||||
],
|
||||
|
||||
// Woche 1 - Wiederholung (Lektion 9)
|
||||
'Woche 1 - Wiederholung': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wiederholung: Wie sagt man "Wie geht es dir?"?',
|
||||
instruction: 'Wähle die richtige Begrüßung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wie geht es dir?" auf Bisaya?',
|
||||
options: ['Kumusta ka?', 'Maayo', 'Salamat', 'Palihug']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Kumusta ka?" ist die Standard-Begrüßung auf Bisaya.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wiederholung: Wie sagt man "Mutter" auf Bisaya?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Mutter" auf Bisaya?',
|
||||
options: ['Nanay', 'Tatay', 'Kuya', 'Ate']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Nanay" bedeutet "Mutter" auf Bisaya.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wiederholung: Was bedeutet "Palangga taka"?',
|
||||
instruction: 'Wähle die richtige Bedeutung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Palangga taka"?',
|
||||
options: ['Ich hab dich lieb', 'Danke', 'Guten Tag', 'Auf Wiedersehen']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Palangga taka" bedeutet "Ich hab dich lieb" - wärmer als "I love you" im Familienkontext.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wiederholung: Was fragt man mit "Nikaon ka?"?',
|
||||
instruction: 'Wähle die richtige Bedeutung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Nikaon ka?"?',
|
||||
options: ['Hast du schon gegessen?', 'Wie geht es dir?', 'Danke', 'Bitte']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Nikaon ka?" bedeutet "Hast du schon gegessen?" - typisch fürsorglich auf den Philippinen.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wiederholung: Wie sagt man "Ich verstehe nicht"?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
|
||||
options: ['Wala ko kasabot', 'Salamat', 'Maayo', 'Palihug']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht".'
|
||||
}
|
||||
],
|
||||
|
||||
// Woche 1 - Vokabeltest (Lektion 10)
|
||||
'Woche 1 - Vokabeltest': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Vokabeltest: Kumusta',
|
||||
instruction: 'Was bedeutet "Kumusta"?',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Kumusta"?',
|
||||
options: ['Wie geht es dir?', 'Danke', 'Bitte', 'Auf Wiedersehen']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Kumusta" kommt von spanisch "¿Cómo está?" - "Wie geht es dir?"'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Vokabeltest: Lola',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Lola"?',
|
||||
options: ['Großmutter', 'Großvater', 'Mutter', 'Vater']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Lola" = Großmutter, "Lolo" = Großvater.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Vokabeltest: Salamat',
|
||||
instruction: 'Wähle die richtige Bedeutung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Salamat"?',
|
||||
options: ['Danke', 'Bitte', 'Entschuldigung', 'Gern geschehen']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Salamat" bedeutet "Danke".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Vokabeltest: Lami',
|
||||
instruction: 'Was bedeutet "Lami"?',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Lami"?',
|
||||
options: ['Lecker', 'Viel', 'Gut', 'Schnell']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Lami" bedeutet "lecker" oder "schmackhaft" - wichtig beim Essen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Vokabeltest: Mingaw ko nimo',
|
||||
instruction: 'Wähle die richtige Bedeutung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Mingaw ko nimo"?',
|
||||
options: ['Ich vermisse dich', 'Ich freue mich', 'Ich mag dich', 'Ich liebe dich']
|
||||
},
|
||||
answerData: { type: 'multiple_choice', correctAnswer: 0 },
|
||||
explanation: '"Mingaw ko nimo" bedeutet "Ich vermisse dich".'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'system'
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'admin'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!systemUser) {
|
||||
console.error('❌ System-Benutzer nicht gefunden.');
|
||||
throw new Error('System user not found');
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
function getExercisesForLesson(lessonTitle) {
|
||||
// Suche nach exaktem Titel
|
||||
if (BISAYA_EXERCISES[lessonTitle]) {
|
||||
return BISAYA_EXERCISES[lessonTitle];
|
||||
}
|
||||
|
||||
// Fallback: Suche nach Teilstring
|
||||
for (const [key, exercises] of Object.entries(BISAYA_EXERCISES)) {
|
||||
if (lessonTitle.includes(key) || key.includes(lessonTitle)) {
|
||||
return exercises;
|
||||
}
|
||||
}
|
||||
|
||||
// Keine Übungen für unbekannte Lektionen (statt Dummy-Übungen)
|
||||
return [];
|
||||
}
|
||||
|
||||
async function createBisayaCourseContent() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
|
||||
|
||||
// Finde alle Bisaya-Kurse
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
if (!bisayaLanguage) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const courses = await sequelize.query(
|
||||
`SELECT id, title, owner_user_id AS "ownerUserId" FROM community.vocab_course WHERE language_id = :languageId`,
|
||||
{
|
||||
replacements: { languageId: bisayaLanguage.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
|
||||
|
||||
let totalExercisesAdded = 0;
|
||||
let totalLessonsProcessed = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: { courseId: course.id },
|
||||
order: [['lessonNumber', 'ASC']]
|
||||
});
|
||||
|
||||
console.log(` ${lessons.length} Lektionen gefunden\n`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
const exercises = getExercisesForLesson(lesson.title);
|
||||
if (exercises.length === 0) {
|
||||
const existingCount = await VocabGrammarExercise.count({ where: { lessonId: lesson.id } });
|
||||
if (existingCount > 0) {
|
||||
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
|
||||
} else {
|
||||
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bei Woche-1-Wiederholung/Vokabeltest: Alte Platzhalter entfernen und ersetzen
|
||||
const replacePlaceholders = ['Woche 1 - Wiederholung', 'Woche 1 - Vokabeltest'].includes(lesson.title);
|
||||
const existingCount = await VocabGrammarExercise.count({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
|
||||
if (existingCount > 0 && !replacePlaceholders) {
|
||||
console.log(` ⏭️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - bereits ${existingCount} Übung(en) vorhanden`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (replacePlaceholders && existingCount > 0) {
|
||||
const deleted = await VocabGrammarExercise.destroy({ where: { lessonId: lesson.id } });
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deleted} Platzhalter entfernt`);
|
||||
}
|
||||
|
||||
// Erstelle Übungen
|
||||
let exerciseNumber = 1;
|
||||
for (const exerciseData of exercises) {
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: exerciseData.exerciseTypeId,
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: exerciseData.title,
|
||||
instruction: exerciseData.instruction,
|
||||
questionData: JSON.stringify(exerciseData.questionData),
|
||||
answerData: JSON.stringify(exerciseData.answerData),
|
||||
explanation: exerciseData.explanation,
|
||||
createdByUserId: course.ownerUserId || systemUser.id
|
||||
});
|
||||
totalExercisesAdded++;
|
||||
}
|
||||
|
||||
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} Übung(en) erstellt`);
|
||||
totalLessonsProcessed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalLessonsProcessed} Lektionen bearbeitet`);
|
||||
console.log(` ${totalExercisesAdded} Grammatik-Übungen erstellt`);
|
||||
}
|
||||
|
||||
createBisayaCourseContent()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
309
backend/scripts/create-bisaya-course.js
Executable file
309
backend/scripts/create-bisaya-course.js
Executable file
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Erstellen eines vollständigen 4-Wochen Bisaya-Kurses
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/create-bisaya-course.js <languageId> <ownerHashedId>
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import User from '../models/community/user.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const LESSONS = [
|
||||
// WOCHE 1: Grundlagen & Aussprache
|
||||
{ week: 1, day: 1, num: 1, type: 'conversation', title: 'Begrüßungen & Höflichkeit',
|
||||
desc: 'Lerne die wichtigsten Begrüßungen und Höflichkeitsformeln',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Philippiner schätzen Höflichkeit sehr. Lächeln ist wichtig!' },
|
||||
|
||||
{ week: 1, day: 1, num: 2, type: 'vocab', title: 'Überlebenssätze - Teil 1',
|
||||
desc: 'Die 10 wichtigsten Sätze für den Alltag',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: 'Diese Sätze helfen dir sofort im Alltag weiter.' },
|
||||
|
||||
{ week: 1, day: 2, num: 3, type: 'vocab', title: 'Familienwörter',
|
||||
desc: 'Mama, Papa, Kuya, Ate, Lola, Lolo und mehr',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: 'Kuya und Ate werden auch für Nicht-Verwandte verwendet – sehr respektvoll!' },
|
||||
|
||||
{ week: 1, day: 2, num: 4, type: 'conversation', title: 'Familien-Gespräche',
|
||||
desc: 'Einfache Gespräche mit Familienmitgliedern',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Familienkonversationen sind herzlicher als formelle Gespräche.' },
|
||||
|
||||
{ week: 1, day: 3, num: 5, type: 'conversation', title: 'Gefühle & Zuneigung',
|
||||
desc: 'Mingaw ko nimo, Palangga taka und mehr',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Palangga taka ist wärmer als "I love you" im Familienkontext.' },
|
||||
|
||||
{ week: 1, day: 3, num: 6, type: 'vocab', title: 'Überlebenssätze - Teil 2',
|
||||
desc: 'Weitere wichtige Alltagssätze',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 1, day: 4, num: 7, type: 'conversation', title: 'Essen & Fürsorge',
|
||||
desc: 'Nikaon ka? Kaon ta! Lami!',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Essen = Liebe! "Nikaon na ka?" ist sehr fürsorglich.' },
|
||||
|
||||
{ week: 1, day: 4, num: 8, type: 'vocab', title: 'Essen & Trinken',
|
||||
desc: 'Wichtige Wörter rund ums Essen',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 1, day: 5, num: 9, type: 'review', title: 'Woche 1 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der ersten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: 'Wiederholung ist der Schlüssel zum Erfolg!' },
|
||||
|
||||
{ week: 1, day: 5, num: 10, type: 'vocab', title: 'Woche 1 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 1',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 2: Alltag & Familie
|
||||
{ week: 2, day: 1, num: 11, type: 'conversation', title: 'Alltagsgespräche - Teil 1',
|
||||
desc: 'Wie war dein Tag? Was machst du?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' },
|
||||
|
||||
{ week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie',
|
||||
desc: 'Balay, Kwarto, Kusina, Pamilya',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2',
|
||||
desc: 'Wohin gehst du? Was machst du heute?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung',
|
||||
desc: 'Asa, dinhi, didto, padulong',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen',
|
||||
desc: 'Ni-kaon ko, Mo-kaon ko - Vergangenheit und Zukunft',
|
||||
targetMin: 25, targetScore: 75, review: true,
|
||||
cultural: 'Cebuano hat keine komplexen Zeiten wie Deutsch. Zeit wird mit Präfixen ausgedrückt.' },
|
||||
|
||||
{ week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum',
|
||||
desc: 'Karon, ugma, gahapon, karon adlaw',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise',
|
||||
desc: 'Tagpila ni? Pwede barato?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Handeln ist in den Philippinen üblich und erwartet.' },
|
||||
|
||||
{ week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise',
|
||||
desc: '1-100, Preise, Mengen',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der zweiten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 2',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 3: Vertiefung
|
||||
{ week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen',
|
||||
desc: 'Nalipay, nasubo, nahadlok, naguol',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' },
|
||||
|
||||
{ week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen',
|
||||
desc: 'Wörter für verschiedene Gefühle',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden',
|
||||
desc: 'Sakit, maayo, tambal, doktor',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit',
|
||||
desc: 'Wörter rund um den Körper und Gesundheit',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen',
|
||||
desc: 'Palihug, Pwede, Tabang',
|
||||
targetMin: 20, targetScore: 75, review: true,
|
||||
cultural: 'Höflichkeit ist extrem wichtig in der philippinischen Kultur.' },
|
||||
|
||||
{ week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen',
|
||||
desc: 'Wie man höflich fragt und bittet',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie',
|
||||
desc: 'Gespräche mit und über Kinder',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Kinder sind sehr wichtig in philippinischen Familien.' },
|
||||
|
||||
{ week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel',
|
||||
desc: 'Wörter für Kinder und Spielsachen',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der dritten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 3',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 4: Freies Sprechen
|
||||
{ week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1',
|
||||
desc: 'Übe freies Sprechen zu verschiedenen Themen',
|
||||
targetMin: 20, targetScore: 75, review: false,
|
||||
cultural: 'Fehler sind okay! Philippiner schätzen das Bemühen.' },
|
||||
|
||||
{ week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2',
|
||||
desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2',
|
||||
desc: 'Weitere Übung im freien Sprechen',
|
||||
targetMin: 20, targetScore: 75, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3',
|
||||
desc: 'Wiederhole wichtige Vokabeln aus Woche 3',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche',
|
||||
desc: 'Längere Gespräche zu verschiedenen Themen',
|
||||
targetMin: 25, targetScore: 75, review: false,
|
||||
cultural: 'Je mehr du sprichst, desto besser wirst du!' },
|
||||
|
||||
{ week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung',
|
||||
desc: 'Wiederhole alle wichtigen Inhalte des Kurses',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung',
|
||||
desc: 'Simuliere echte Gesprächssituationen',
|
||||
targetMin: 25, targetScore: 75, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln',
|
||||
desc: 'Finaler Vokabeltest über den gesamten Kurs',
|
||||
targetMin: 20, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung',
|
||||
desc: 'Finale Prüfung über alle Kursinhalte',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: 'Gratulation zum Abschluss des Kurses!' },
|
||||
|
||||
{ week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks',
|
||||
desc: 'Wichtige kulturelle Hinweise für den Alltag',
|
||||
targetMin: 15, targetScore: 0, review: false,
|
||||
cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' }
|
||||
];
|
||||
|
||||
async function createBisayaCourse(languageId, ownerHashedId) {
|
||||
try {
|
||||
// Finde User
|
||||
const user = await User.findOne({ where: { hashedId: ownerHashedId } });
|
||||
if (!user) {
|
||||
throw new Error(`User mit hashedId ${ownerHashedId} nicht gefunden`);
|
||||
}
|
||||
|
||||
// Prüfe, ob Sprache existiert
|
||||
const [lang] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE id = :langId`,
|
||||
{ replacements: { langId: languageId }, type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
if (!lang) {
|
||||
throw new Error(`Sprache mit ID ${languageId} nicht gefunden`);
|
||||
}
|
||||
|
||||
// Erstelle Kurs
|
||||
const shareCode = crypto.randomBytes(8).toString('hex');
|
||||
const course = await VocabCourse.create({
|
||||
ownerUserId: user.id,
|
||||
title: 'Bisaya für Familien - Schnellstart in 4 Wochen',
|
||||
description: 'Lerne Bisaya (Cebuano) schnell und praktisch für den Familienalltag. Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.',
|
||||
languageId: Number(languageId),
|
||||
difficultyLevel: 1,
|
||||
isPublic: true,
|
||||
shareCode
|
||||
});
|
||||
|
||||
console.log(`✅ Kurs erstellt: ${course.id} - "${course.title}"`);
|
||||
console.log(` Share-Code: ${shareCode}`);
|
||||
|
||||
// Erstelle Lektionen
|
||||
for (const lessonData of LESSONS) {
|
||||
const lesson = await VocabCourseLesson.create({
|
||||
courseId: course.id,
|
||||
chapterId: null, // Wird später mit Vokabeln verknüpft
|
||||
lessonNumber: lessonData.num,
|
||||
title: lessonData.title,
|
||||
description: lessonData.desc,
|
||||
weekNumber: lessonData.week,
|
||||
dayNumber: lessonData.day,
|
||||
lessonType: lessonData.type,
|
||||
culturalNotes: lessonData.cultural,
|
||||
targetMinutes: lessonData.targetMin,
|
||||
targetScorePercent: lessonData.targetScore,
|
||||
requiresReview: lessonData.review
|
||||
});
|
||||
console.log(` ✅ Lektion ${lessonData.num}: ${lessonData.title} (Woche ${lessonData.week}, Tag ${lessonData.day})`);
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Kurs erfolgreich erstellt mit ${LESSONS.length} Lektionen!`);
|
||||
console.log(`\n📊 Kurs-Statistik:`);
|
||||
console.log(` - Gesamte Lektionen: ${LESSONS.length}`);
|
||||
console.log(` - Vokabel-Lektionen: ${LESSONS.filter(l => l.type === 'vocab').length}`);
|
||||
console.log(` - Konversations-Lektionen: ${LESSONS.filter(l => l.type === 'conversation').length}`);
|
||||
console.log(` - Grammatik-Lektionen: ${LESSONS.filter(l => l.type === 'grammar').length}`);
|
||||
console.log(` - Wiederholungs-Lektionen: ${LESSONS.filter(l => l.type === 'review').length}`);
|
||||
console.log(` - Durchschnittliche Zeit pro Tag: ~${Math.round(LESSONS.reduce((sum, l) => sum + l.targetMin, 0) / (4 * 5))} Minuten`);
|
||||
console.log(`\n💡 Nächste Schritte:`);
|
||||
console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`);
|
||||
console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`);
|
||||
console.log(` 3. Teile den Kurs mit anderen (Share-Code: ${shareCode})`);
|
||||
|
||||
return course;
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Erstellen des Kurses:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI-Aufruf
|
||||
const languageId = process.argv[2];
|
||||
const ownerHashedId = process.argv[3];
|
||||
|
||||
if (!languageId || !ownerHashedId) {
|
||||
console.error('Verwendung: node create-bisaya-course.js <languageId> <ownerHashedId>');
|
||||
console.error('Beispiel: node create-bisaya-course.js 1 abc123def456');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
createBisayaCourse(languageId, ownerHashedId)
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
557
backend/scripts/create-language-courses.js
Executable file
557
backend/scripts/create-language-courses.js
Executable file
@@ -0,0 +1,557 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Erstellen von öffentlichen Sprachkursen für verschiedene Sprachen
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/create-language-courses.js
|
||||
*
|
||||
* Erstellt öffentliche Kurse für alle Kombinationen von:
|
||||
* - Zielsprachen: Bisaya, Französisch, Spanisch, Latein, Italienisch, Portugiesisch, Tagalog
|
||||
* - Muttersprachen: Deutsch, Englisch, Spanisch, Französisch, Italienisch, Portugiesisch
|
||||
*
|
||||
* Die Kurse werden automatisch einem System-Benutzer zugeordnet und sind öffentlich zugänglich.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import User from '../models/community/user.js';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// Kursstruktur für alle Sprachen (4 Wochen, 40 Lektionen)
|
||||
const LESSON_TEMPLATE = [
|
||||
// WOCHE 1: Grundlagen & Aussprache
|
||||
{ week: 1, day: 1, num: 1, type: 'conversation', title: 'Begrüßungen & Höflichkeit',
|
||||
desc: 'Lerne die wichtigsten Begrüßungen und Höflichkeitsformeln',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Höflichkeit ist wichtig. Lächeln hilft!' },
|
||||
|
||||
{ week: 1, day: 1, num: 2, type: 'vocab', title: 'Überlebenssätze - Teil 1',
|
||||
desc: 'Die 10 wichtigsten Sätze für den Alltag',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: 'Diese Sätze helfen dir sofort im Alltag weiter.' },
|
||||
|
||||
{ week: 1, day: 2, num: 3, type: 'vocab', title: 'Familienwörter',
|
||||
desc: 'Mama, Papa, Geschwister, Großeltern und mehr',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: 'Familienwörter sind wichtig für echte Gespräche.' },
|
||||
|
||||
{ week: 1, day: 2, num: 4, type: 'conversation', title: 'Familien-Gespräche',
|
||||
desc: 'Einfache Gespräche mit Familienmitgliedern',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Familienkonversationen sind herzlicher als formelle Gespräche.' },
|
||||
|
||||
{ week: 1, day: 3, num: 5, type: 'conversation', title: 'Gefühle & Zuneigung',
|
||||
desc: 'Wie man Gefühle ausdrückt',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Gefühle auszudrücken ist wichtig für echte Verbindung.' },
|
||||
|
||||
{ week: 1, day: 3, num: 6, type: 'vocab', title: 'Überlebenssätze - Teil 2',
|
||||
desc: 'Weitere wichtige Alltagssätze',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 1, day: 4, num: 7, type: 'conversation', title: 'Essen & Fürsorge',
|
||||
desc: 'Gespräche rund ums Essen',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Essen verbindet Menschen überall auf der Welt.' },
|
||||
|
||||
{ week: 1, day: 4, num: 8, type: 'vocab', title: 'Essen & Trinken',
|
||||
desc: 'Wichtige Wörter rund ums Essen',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 1, day: 5, num: 9, type: 'review', title: 'Woche 1 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der ersten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: 'Wiederholung ist der Schlüssel zum Erfolg!' },
|
||||
|
||||
{ week: 1, day: 5, num: 10, type: 'vocab', title: 'Woche 1 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 1',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 2: Alltag & Familie
|
||||
{ week: 2, day: 1, num: 11, type: 'conversation', title: 'Alltagsgespräche - Teil 1',
|
||||
desc: 'Wie war dein Tag? Was machst du?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' },
|
||||
|
||||
{ week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie',
|
||||
desc: 'Wörter für Haus, Zimmer, Familie',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2',
|
||||
desc: 'Wohin gehst du? Was machst du heute?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung',
|
||||
desc: 'Wo, hier, dort, gehen zu',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen',
|
||||
desc: 'Vergangenheit, Gegenwart, Zukunft',
|
||||
targetMin: 25, targetScore: 75, review: true,
|
||||
cultural: 'Zeitformen sind wichtig für präzise Kommunikation.' },
|
||||
|
||||
{ week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum',
|
||||
desc: 'Jetzt, morgen, gestern, heute',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise',
|
||||
desc: 'Wie viel kostet das? Kann es billiger sein?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Einkaufen ist eine wichtige Alltagssituation.' },
|
||||
|
||||
{ week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise',
|
||||
desc: 'Zahlen 1-100, Preise, Mengen',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der zweiten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 2',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 3: Vertiefung
|
||||
{ week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen',
|
||||
desc: 'Wie man verschiedene Gefühle ausdrückt',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' },
|
||||
|
||||
{ week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen',
|
||||
desc: 'Wörter für verschiedene Gefühle',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden',
|
||||
desc: 'Gespräche über Gesundheit',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit',
|
||||
desc: 'Wörter rund um den Körper und Gesundheit',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen',
|
||||
desc: 'Wie man höflich spricht',
|
||||
targetMin: 20, targetScore: 75, review: true,
|
||||
cultural: 'Höflichkeit ist extrem wichtig in jeder Kultur.' },
|
||||
|
||||
{ week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen',
|
||||
desc: 'Wie man höflich fragt und bittet',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie',
|
||||
desc: 'Gespräche mit und über Kinder',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Kinder sind sehr wichtig in Familien.' },
|
||||
|
||||
{ week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel',
|
||||
desc: 'Wörter für Kinder und Spielsachen',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der dritten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 3',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 4: Freies Sprechen
|
||||
{ week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1',
|
||||
desc: 'Übe freies Sprechen zu verschiedenen Themen',
|
||||
targetMin: 20, targetScore: 75, review: false,
|
||||
cultural: 'Fehler sind okay! Muttersprachler schätzen das Bemühen.' },
|
||||
|
||||
{ week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2',
|
||||
desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2',
|
||||
desc: 'Weitere Übung im freien Sprechen',
|
||||
targetMin: 20, targetScore: 75, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3',
|
||||
desc: 'Wiederhole wichtige Vokabeln aus Woche 3',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche',
|
||||
desc: 'Längere Gespräche zu verschiedenen Themen',
|
||||
targetMin: 25, targetScore: 75, review: false,
|
||||
cultural: 'Je mehr du sprichst, desto besser wirst du!' },
|
||||
|
||||
{ week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung',
|
||||
desc: 'Wiederhole alle wichtigen Inhalte des Kurses',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung',
|
||||
desc: 'Simuliere echte Gesprächssituationen',
|
||||
targetMin: 25, targetScore: 75, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln',
|
||||
desc: 'Finaler Vokabeltest über den gesamten Kurs',
|
||||
targetMin: 20, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung',
|
||||
desc: 'Finale Prüfung über alle Kursinhalte',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: 'Gratulation zum Abschluss des Kurses!' },
|
||||
|
||||
{ week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks',
|
||||
desc: 'Wichtige kulturelle Hinweise für den Alltag',
|
||||
targetMin: 15, targetScore: 0, review: false,
|
||||
cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' }
|
||||
];
|
||||
|
||||
// Zielsprachen (die zu lernenden Sprachen)
|
||||
const TARGET_LANGUAGES = [
|
||||
'Bisaya',
|
||||
'Französisch',
|
||||
'Spanisch',
|
||||
'Latein',
|
||||
'Italienisch',
|
||||
'Portugiesisch',
|
||||
'Tagalog'
|
||||
];
|
||||
|
||||
// Muttersprachen (für die Kurse erstellt werden)
|
||||
const NATIVE_LANGUAGES = [
|
||||
'Deutsch',
|
||||
'Englisch',
|
||||
'Spanisch',
|
||||
'Französisch',
|
||||
'Italienisch',
|
||||
'Portugiesisch'
|
||||
];
|
||||
|
||||
// Generiere Kurskonfigurationen für alle Kombinationen
|
||||
function generateCourseConfigs() {
|
||||
const configs = [];
|
||||
|
||||
for (const targetLang of TARGET_LANGUAGES) {
|
||||
for (const nativeLang of NATIVE_LANGUAGES) {
|
||||
// Überspringe, wenn Zielsprache = Muttersprache
|
||||
if (targetLang === nativeLang) continue;
|
||||
|
||||
const title = `${targetLang} für ${nativeLang}sprachige - Schnellstart in 4 Wochen`;
|
||||
let description = `Lerne ${targetLang} schnell und praktisch für den Alltag. `;
|
||||
|
||||
if (targetLang === 'Latein') {
|
||||
description = `Lerne ${targetLang} systematisch mit Fokus auf Grammatik und Vokabular. `;
|
||||
} else if (targetLang === 'Bisaya') {
|
||||
description = `Lerne ${targetLang} (Cebuano) schnell und praktisch für den Familienalltag. `;
|
||||
}
|
||||
|
||||
description += `Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.`;
|
||||
|
||||
configs.push({
|
||||
targetLanguageName: targetLang,
|
||||
nativeLanguageName: nativeLang,
|
||||
title,
|
||||
description,
|
||||
difficultyLevel: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
const LANGUAGE_COURSES = generateCourseConfigs();
|
||||
|
||||
async function findOrCreateLanguage(languageName, ownerUserId) {
|
||||
// Suche zuerst nach vorhandener Sprache
|
||||
const [existing] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = :name LIMIT 1`,
|
||||
{
|
||||
replacements: { name: languageName },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
console.log(` ✅ Sprache "${languageName}" bereits vorhanden (ID: ${existing.id})`);
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
// Erstelle neue Sprache
|
||||
const shareCode = crypto.randomBytes(8).toString('hex');
|
||||
const [created] = await sequelize.query(
|
||||
`INSERT INTO community.vocab_language (owner_user_id, name, share_code)
|
||||
VALUES (:ownerUserId, :name, :shareCode)
|
||||
RETURNING id`,
|
||||
{
|
||||
replacements: { ownerUserId, name: languageName, shareCode },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` ✅ Sprache "${languageName}" erstellt (ID: ${created.id})`);
|
||||
return created.id;
|
||||
}
|
||||
|
||||
async function createCourseForLanguage(targetLanguageId, nativeLanguageId, languageConfig, ownerUserId) {
|
||||
const shareCode = crypto.randomBytes(8).toString('hex');
|
||||
|
||||
const course = await VocabCourse.create({
|
||||
ownerUserId,
|
||||
title: languageConfig.title,
|
||||
description: languageConfig.description,
|
||||
languageId: Number(targetLanguageId),
|
||||
nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null,
|
||||
difficultyLevel: languageConfig.difficultyLevel || 1,
|
||||
isPublic: true,
|
||||
shareCode
|
||||
});
|
||||
|
||||
console.log(` ✅ Kurs erstellt: "${course.title}" (ID: ${course.id}, Share-Code: ${shareCode})`);
|
||||
|
||||
// Erstelle Lektionen
|
||||
const createdLessons = [];
|
||||
for (const lessonData of LESSON_TEMPLATE) {
|
||||
const lesson = await VocabCourseLesson.create({
|
||||
courseId: course.id,
|
||||
chapterId: null,
|
||||
lessonNumber: lessonData.num,
|
||||
title: lessonData.title,
|
||||
description: lessonData.desc,
|
||||
weekNumber: lessonData.week,
|
||||
dayNumber: lessonData.day,
|
||||
lessonType: lessonData.type,
|
||||
culturalNotes: lessonData.cultural,
|
||||
targetMinutes: lessonData.targetMin,
|
||||
targetScorePercent: lessonData.targetScore,
|
||||
requiresReview: lessonData.review
|
||||
});
|
||||
createdLessons.push({ lesson, lessonData });
|
||||
}
|
||||
|
||||
console.log(` ✅ ${LESSON_TEMPLATE.length} Lektionen erstellt`);
|
||||
|
||||
// Erstelle Beispiel-Grammatik-Übungen für Grammar-Lektionen
|
||||
let grammarExerciseCount = 0;
|
||||
for (const { lesson, lessonData } of createdLessons) {
|
||||
if (lessonData.type === 'grammar') {
|
||||
// Erstelle 2-3 Beispiel-Übungen für jede Grammar-Lektion
|
||||
const exercises = createExampleGrammarExercises(lesson.id, lessonData, ownerUserId);
|
||||
for (const exercise of exercises) {
|
||||
await VocabGrammarExercise.create(exercise);
|
||||
grammarExerciseCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (grammarExerciseCount > 0) {
|
||||
console.log(` ✅ ${grammarExerciseCount} Grammatik-Übungen erstellt`);
|
||||
}
|
||||
|
||||
return course;
|
||||
}
|
||||
|
||||
// Erstelle Beispiel-Grammatik-Übungen für eine Grammar-Lektion
|
||||
function createExampleGrammarExercises(lessonId, lessonData, ownerUserId) {
|
||||
const exercises = [];
|
||||
|
||||
// Beispiel-Übung 1: Gap Fill (Lückentext)
|
||||
exercises.push({
|
||||
lessonId: lessonId,
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
exerciseNumber: 1,
|
||||
title: `${lessonData.title} - Übung 1`,
|
||||
instruction: 'Fülle die Lücken mit den richtigen Wörtern.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
text: 'Hallo! Wie geht es {gap}? Mir geht es {gap}, danke!',
|
||||
gaps: 2
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
answers: ['dir', 'gut']
|
||||
}),
|
||||
explanation: 'Die richtigen Antworten sind "dir" und "gut".',
|
||||
createdByUserId: ownerUserId
|
||||
});
|
||||
|
||||
// Beispiel-Übung 2: Multiple Choice
|
||||
exercises.push({
|
||||
lessonId: lessonId,
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: 2,
|
||||
title: `${lessonData.title} - Übung 2`,
|
||||
instruction: 'Wähle die richtige Antwort aus.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Guten Tag" auf ' + lessonData.title.split(' - ')[0] + '?',
|
||||
options: ['Option A', 'Option B', 'Option C', 'Option D']
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
}),
|
||||
explanation: 'Die richtige Antwort ist Option A.',
|
||||
createdByUserId: ownerUserId
|
||||
});
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
// Versuche zuerst einen System-Benutzer zu finden (z.B. mit username "system" oder "admin")
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'system'
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
// Versuche Admin-Benutzer
|
||||
systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'admin'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!systemUser) {
|
||||
// Erstelle einen System-Benutzer
|
||||
console.log(' ℹ️ Erstelle System-Benutzer für öffentliche Kurse...');
|
||||
const saltRounds = 10;
|
||||
const randomPassword = crypto.randomBytes(32).toString('hex');
|
||||
const hashedPassword = await bcrypt.hash(randomPassword, saltRounds);
|
||||
|
||||
systemUser = await User.create({
|
||||
username: 'system',
|
||||
email: 'system@your-part.de',
|
||||
password: hashedPassword,
|
||||
active: true,
|
||||
registrationDate: new Date()
|
||||
});
|
||||
|
||||
// hashedId wird automatisch vom Hook gesetzt, aber warte kurz darauf
|
||||
await systemUser.reload();
|
||||
console.log(` ✅ System-Benutzer erstellt (ID: ${systemUser.id}, hashedId: ${systemUser.hashedId})`);
|
||||
} else {
|
||||
console.log(` ✅ System-Benutzer gefunden (ID: ${systemUser.id}, Username: ${systemUser.username})`);
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
async function createAllLanguageCourses() {
|
||||
try {
|
||||
// Finde oder erstelle System-Benutzer
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
|
||||
console.log(`\n🚀 Erstelle öffentliche Sprachkurse (Besitzer: System-Benutzer ID ${systemUser.id})\n`);
|
||||
|
||||
const createdCourses = [];
|
||||
|
||||
// Stelle sicher, dass alle benötigten Sprachen existieren
|
||||
console.log(`\n🌍 Stelle sicher, dass alle Sprachen existieren...`);
|
||||
const allLanguages = [...new Set([...TARGET_LANGUAGES, ...NATIVE_LANGUAGES])];
|
||||
const languageMap = new Map();
|
||||
|
||||
for (const langName of allLanguages) {
|
||||
const langId = await findOrCreateLanguage(langName, systemUser.id);
|
||||
languageMap.set(langName, langId);
|
||||
}
|
||||
|
||||
for (const langConfig of LANGUAGE_COURSES) {
|
||||
console.log(`\n📚 Verarbeite: ${langConfig.targetLanguageName} für ${langConfig.nativeLanguageName}sprachige`);
|
||||
|
||||
const targetLanguageId = languageMap.get(langConfig.targetLanguageName);
|
||||
const nativeLanguageId = languageMap.get(langConfig.nativeLanguageName);
|
||||
|
||||
// Prüfe, ob Kurs bereits existiert (unabhängig vom Besitzer, wenn öffentlich)
|
||||
const existingCourse = await VocabCourse.findOne({
|
||||
where: {
|
||||
languageId: targetLanguageId,
|
||||
nativeLanguageId: nativeLanguageId,
|
||||
isPublic: true
|
||||
}
|
||||
});
|
||||
|
||||
if (existingCourse) {
|
||||
console.log(` ⚠️ Kurs "${langConfig.title}" existiert bereits (ID: ${existingCourse.id})`);
|
||||
createdCourses.push({
|
||||
...langConfig,
|
||||
courseId: existingCourse.id,
|
||||
targetLanguageId,
|
||||
nativeLanguageId,
|
||||
skipped: true
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Erstelle Kurs
|
||||
const course = await createCourseForLanguage(targetLanguageId, nativeLanguageId, langConfig, systemUser.id);
|
||||
createdCourses.push({
|
||||
...langConfig,
|
||||
courseId: course.id,
|
||||
targetLanguageId,
|
||||
nativeLanguageId,
|
||||
shareCode: course.shareCode
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n\n🎉 Zusammenfassung:\n`);
|
||||
console.log(` Gesamt: ${LANGUAGE_COURSES.length} Sprachen`);
|
||||
console.log(` Erstellt: ${createdCourses.filter(c => !c.skipped).length} Kurse`);
|
||||
console.log(` Übersprungen: ${createdCourses.filter(c => c.skipped).length} Kurse`);
|
||||
|
||||
console.log(`\n📋 Erstellte Kurse:\n`);
|
||||
for (const course of createdCourses) {
|
||||
if (course.skipped) {
|
||||
console.log(` ⚠️ ${course.languageName}: Bereits vorhanden (ID: ${course.courseId})`);
|
||||
} else {
|
||||
console.log(` ✅ ${course.languageName}: ${course.title}`);
|
||||
console.log(` Share-Code: ${course.shareCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n💡 Nächste Schritte:`);
|
||||
console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`);
|
||||
console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`);
|
||||
console.log(` 3. Teile die Kurse mit anderen (Share-Codes oben)`);
|
||||
|
||||
return createdCourses;
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Erstellen der Kurse:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI-Aufruf
|
||||
// Keine Parameter mehr nötig - verwendet automatisch System-Benutzer
|
||||
createAllLanguageCourses()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
88
backend/scripts/delete-all-family-words-exercises.js
Executable file
88
backend/scripts/delete-all-family-words-exercises.js
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Löschen ALLER "Familienwörter"-Übungen aus Bisaya-Kursen
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/delete-all-family-words-exercises.js
|
||||
*
|
||||
* Löscht alle Grammatik-Übungen von "Familienwörter"-Lektionen, um Platz für neue zu schaffen.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
|
||||
async function deleteAllFamilyWordsExercises() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
// Finde alle Bisaya-Kurse
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
if (!bisayaLanguage) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const courses = await sequelize.query(
|
||||
`SELECT id, title FROM community.vocab_course WHERE language_id = :languageId`,
|
||||
{
|
||||
replacements: { languageId: bisayaLanguage.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
|
||||
|
||||
let totalDeleted = 0;
|
||||
let totalLessons = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
|
||||
// Finde "Familienwörter"-Lektionen
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: course.id,
|
||||
title: 'Familienwörter'
|
||||
},
|
||||
order: [['lessonNumber', 'ASC']]
|
||||
});
|
||||
|
||||
console.log(` ${lessons.length} "Familienwörter"-Lektionen gefunden`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
// Lösche ALLE bestehenden Übungen
|
||||
const deletedCount = await VocabGrammarExercise.destroy({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} Übung(en) gelöscht`);
|
||||
totalDeleted += deletedCount;
|
||||
totalLessons++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalLessons} Lektionen bearbeitet`);
|
||||
console.log(` ${totalDeleted} Übungen gelöscht`);
|
||||
console.log(`\n💡 Hinweis: Führe jetzt das update-family-words-exercises.js Script aus, um neue Übungen zu erstellen.`);
|
||||
}
|
||||
|
||||
deleteAllFamilyWordsExercises()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
89
backend/scripts/fix-begruessungen-gap-fill.js
Executable file
89
backend/scripts/fix-begruessungen-gap-fill.js
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Korrigieren der Gap-Fill-Übung in "Begrüßungen & Höflichkeit"
|
||||
* Fügt Muttersprache-Hinweise hinzu, damit Vokabeln korrekt extrahiert werden können
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
|
||||
async function fixBegruessungenGapFill() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
// Finde alle "Begrüßungen & Höflichkeit" Lektionen
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: { title: 'Begrüßungen & Höflichkeit' },
|
||||
include: [{ model: VocabCourse, as: 'course' }]
|
||||
});
|
||||
|
||||
console.log(`Gefunden: ${lessons.length} "Begrüßungen & Höflichkeit"-Lektionen\n`);
|
||||
|
||||
let totalUpdated = 0;
|
||||
|
||||
for (const lesson of lessons) {
|
||||
console.log(`📚 Kurs: ${lesson.course.title} (Kurs-ID: ${lesson.course.id}, Lektion-ID: ${lesson.id})`);
|
||||
|
||||
// Finde Gap-Fill-Übung mit "ko" als Antwort
|
||||
const exercises = await VocabGrammarExercise.findAll({
|
||||
where: {
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: 1 // gap_fill
|
||||
}
|
||||
});
|
||||
|
||||
for (const exercise of exercises) {
|
||||
const qData = typeof exercise.questionData === 'string'
|
||||
? JSON.parse(exercise.questionData)
|
||||
: exercise.questionData;
|
||||
const aData = typeof exercise.answerData === 'string'
|
||||
? JSON.parse(exercise.answerData)
|
||||
: exercise.answerData;
|
||||
|
||||
// Prüfe ob es die problematische Übung ist (enthält "ko" als Antwort ohne Muttersprache-Hinweis)
|
||||
if (aData.answers && aData.answers.includes('ko')) {
|
||||
const text = qData.text || '';
|
||||
|
||||
// Prüfe ob Muttersprache-Hinweise fehlen
|
||||
if (!text.includes('(ich)') && !text.includes('(I)')) {
|
||||
console.log(` 🔧 Korrigiere Übung "${exercise.title}" (ID: ${exercise.id})`);
|
||||
|
||||
// Korrigiere den Text
|
||||
const correctedText = text.replace(
|
||||
'Maayo {gap}.',
|
||||
'Maayo {gap} (ich).'
|
||||
);
|
||||
|
||||
qData.text = correctedText;
|
||||
|
||||
await exercise.update({
|
||||
questionData: qData
|
||||
});
|
||||
|
||||
totalUpdated++;
|
||||
console.log(` ✅ Aktualisiert: "${correctedText}"`);
|
||||
} else {
|
||||
console.log(` ✓ Übung "${exercise.title}" bereits korrekt`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${lessons.length} Lektionen verarbeitet`);
|
||||
console.log(` ${totalUpdated} Übungen aktualisiert`);
|
||||
}
|
||||
|
||||
fixBegruessungenGapFill()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
552
backend/scripts/update-family-conversations-exercises.js
Executable file
552
backend/scripts/update-family-conversations-exercises.js
Executable file
@@ -0,0 +1,552 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Erstellen von Übungen für die "Familien-Gespräche" Lektion
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/update-family-conversations-exercises.js
|
||||
*
|
||||
* Erstellt Gesprächsübungen für die "Familien-Gespräche" Lektion in allen Bisaya-Kursen.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import User from '../models/community/user.js';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
// Familiengespräche auf Bisaya mit verschiedenen Muttersprachen
|
||||
const FAMILY_CONVERSATIONS = {
|
||||
// Deutsch -> Bisaya
|
||||
'Deutsch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Kumusta ka, Nanay?',
|
||||
native: 'Wie geht es dir, Mama?',
|
||||
explanation: '"Kumusta ka" ist "Wie geht es dir?" und "Nanay" ist "Mama"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ko, Salamat. Ikaw?',
|
||||
native: 'Mir geht es gut, danke. Und dir?',
|
||||
explanation: '"Maayo ko" bedeutet "Mir geht es gut", "Ikaw" ist "du" (formal)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asa si Tatay?',
|
||||
native: 'Wo ist Papa?',
|
||||
explanation: '"Asa" bedeutet "wo", "si" ist ein Marker für Personen, "Tatay" ist "Papa"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Naa siya sa balay.',
|
||||
native: 'Er ist zu Hause.',
|
||||
explanation: '"Naa" bedeutet "ist/sein", "siya" ist "er/sie", "sa balay" ist "zu Hause"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta na ang Kuya?',
|
||||
native: 'Wie geht es dem älteren Bruder?',
|
||||
explanation: '"Kumusta na" ist "Wie geht es", "ang" ist ein Artikel, "Kuya" ist "älterer Bruder"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ra siya.',
|
||||
native: 'Es geht ihm gut.',
|
||||
explanation: '"Maayo ra" bedeutet "gut/gut geht es", "siya" ist "ihm"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gutom na ko, Nanay.',
|
||||
native: 'Ich bin hungrig, Mama.',
|
||||
explanation: '"Gutom" bedeutet "hungrig", "na" zeigt einen Zustand, "ko" ist "ich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Hulata lang, hapit na ang pagkaon.',
|
||||
native: 'Warte nur, das Essen ist fast fertig.',
|
||||
explanation: '"Hulata" ist "warte", "lang" ist "nur", "hapit na" ist "fast", "pagkaon" ist "Essen"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Englisch -> Bisaya
|
||||
'Englisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Kumusta ka, Nanay?',
|
||||
native: 'How are you, Mom?',
|
||||
explanation: '"Kumusta ka" means "How are you?" and "Nanay" means "Mom"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ko, Salamat. Ikaw?',
|
||||
native: 'I\'m fine, thank you. And you?',
|
||||
explanation: '"Maayo ko" means "I\'m fine", "Ikaw" is "you" (formal)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asa si Tatay?',
|
||||
native: 'Where is Dad?',
|
||||
explanation: '"Asa" means "where", "si" is a person marker, "Tatay" means "Dad"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Naa siya sa balay.',
|
||||
native: 'He is at home.',
|
||||
explanation: '"Naa" means "is/be", "siya" is "he/she", "sa balay" means "at home"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta na ang Kuya?',
|
||||
native: 'How is the older brother?',
|
||||
explanation: '"Kumusta na" means "How is", "ang" is an article, "Kuya" means "older brother"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ra siya.',
|
||||
native: 'He is fine.',
|
||||
explanation: '"Maayo ra" means "fine/well", "siya" means "he"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gutom na ko, Nanay.',
|
||||
native: 'I\'m hungry, Mom.',
|
||||
explanation: '"Gutom" means "hungry", "na" shows a state, "ko" is "I"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Hulata lang, hapit na ang pagkaon.',
|
||||
native: 'Just wait, the food is almost ready.',
|
||||
explanation: '"Hulata" means "wait", "lang" means "just", "hapit na" means "almost", "pagkaon" means "food"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Spanisch -> Bisaya
|
||||
'Spanisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Kumusta ka, Nanay?',
|
||||
native: '¿Cómo estás, Mamá?',
|
||||
explanation: '"Kumusta ka" significa "¿Cómo estás?" y "Nanay" significa "Mamá"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ko, Salamat. Ikaw?',
|
||||
native: 'Estoy bien, gracias. ¿Y tú?',
|
||||
explanation: '"Maayo ko" significa "Estoy bien", "Ikaw" es "tú" (formal)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asa si Tatay?',
|
||||
native: '¿Dónde está Papá?',
|
||||
explanation: '"Asa" significa "dónde", "si" es un marcador de persona, "Tatay" significa "Papá"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Naa siya sa balay.',
|
||||
native: 'Él está en casa.',
|
||||
explanation: '"Naa" significa "está/ser", "siya" es "él/ella", "sa balay" significa "en casa"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta na ang Kuya?',
|
||||
native: '¿Cómo está el hermano mayor?',
|
||||
explanation: '"Kumusta na" significa "¿Cómo está?", "ang" es un artículo, "Kuya" significa "hermano mayor"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ra siya.',
|
||||
native: 'Él está bien.',
|
||||
explanation: '"Maayo ra" significa "bien", "siya" significa "él"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gutom na ko, Nanay.',
|
||||
native: 'Tengo hambre, Mamá.',
|
||||
explanation: '"Gutom" significa "hambriento", "na" muestra un estado, "ko" es "yo"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Hulata lang, hapit na ang pagkaon.',
|
||||
native: 'Solo espera, la comida está casi lista.',
|
||||
explanation: '"Hulata" significa "espera", "lang" significa "solo", "hapit na" significa "casi", "pagkaon" significa "comida"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Französisch -> Bisaya
|
||||
'Französisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Kumusta ka, Nanay?',
|
||||
native: 'Comment vas-tu, Maman?',
|
||||
explanation: '"Kumusta ka" signifie "Comment vas-tu?" et "Nanay" signifie "Maman"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ko, Salamat. Ikaw?',
|
||||
native: 'Je vais bien, merci. Et toi?',
|
||||
explanation: '"Maayo ko" signifie "Je vais bien", "Ikaw" est "tu" (formel)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asa si Tatay?',
|
||||
native: 'Où est Papa?',
|
||||
explanation: '"Asa" signifie "où", "si" est un marqueur de personne, "Tatay" signifie "Papa"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Naa siya sa balay.',
|
||||
native: 'Il est à la maison.',
|
||||
explanation: '"Naa" signifie "est/être", "siya" est "il/elle", "sa balay" signifie "à la maison"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta na ang Kuya?',
|
||||
native: 'Comment va le grand frère?',
|
||||
explanation: '"Kumusta na" signifie "Comment va", "ang" est un article, "Kuya" signifie "grand frère"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ra siya.',
|
||||
native: 'Il va bien.',
|
||||
explanation: '"Maayo ra" signifie "bien", "siya" signifie "il"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gutom na ko, Nanay.',
|
||||
native: 'J\'ai faim, Maman.',
|
||||
explanation: '"Gutom" signifie "faim", "na" montre un état, "ko" est "je"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Hulata lang, hapit na ang pagkaon.',
|
||||
native: 'Attends juste, la nourriture est presque prête.',
|
||||
explanation: '"Hulata" signifie "attends", "lang" signifie "juste", "hapit na" signifie "presque", "pagkaon" signifie "nourriture"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Italienisch -> Bisaya
|
||||
'Italienisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Kumusta ka, Nanay?',
|
||||
native: 'Come stai, Mamma?',
|
||||
explanation: '"Kumusta ka" significa "Come stai?" e "Nanay" significa "Mamma"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ko, Salamat. Ikaw?',
|
||||
native: 'Sto bene, grazie. E tu?',
|
||||
explanation: '"Maayo ko" significa "Sto bene", "Ikaw" è "tu" (formale)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asa si Tatay?',
|
||||
native: 'Dove è Papà?',
|
||||
explanation: '"Asa" significa "dove", "si" è un marcatore di persona, "Tatay" significa "Papà"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Naa siya sa balay.',
|
||||
native: 'È a casa.',
|
||||
explanation: '"Naa" significa "è/essere", "siya" è "lui/lei", "sa balay" significa "a casa"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta na ang Kuya?',
|
||||
native: 'Come sta il fratello maggiore?',
|
||||
explanation: '"Kumusta na" significa "Come sta", "ang" è un articolo, "Kuya" significa "fratello maggiore"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ra siya.',
|
||||
native: 'Sta bene.',
|
||||
explanation: '"Maayo ra" significa "bene", "siya" significa "lui"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gutom na ko, Nanay.',
|
||||
native: 'Ho fame, Mamma.',
|
||||
explanation: '"Gutom" significa "fame", "na" mostra uno stato, "ko" è "io"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Hulata lang, hapit na ang pagkaon.',
|
||||
native: 'Aspetta solo, il cibo è quasi pronto.',
|
||||
explanation: '"Hulata" significa "aspetta", "lang" significa "solo", "hapit na" significa "quasi", "pagkaon" significa "cibo"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Portugiesisch -> Bisaya
|
||||
'Portugiesisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Kumusta ka, Nanay?',
|
||||
native: 'Como você está, Mãe?',
|
||||
explanation: '"Kumusta ka" significa "Como você está?" e "Nanay" significa "Mãe"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ko, Salamat. Ikaw?',
|
||||
native: 'Estou bem, obrigado. E você?',
|
||||
explanation: '"Maayo ko" significa "Estou bem", "Ikaw" é "você" (formal)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asa si Tatay?',
|
||||
native: 'Onde está o Papai?',
|
||||
explanation: '"Asa" significa "onde", "si" é um marcador de pessoa, "Tatay" significa "Papai"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Naa siya sa balay.',
|
||||
native: 'Ele está em casa.',
|
||||
explanation: '"Naa" significa "está/ser", "siya" é "ele/ela", "sa balay" significa "em casa"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta na ang Kuya?',
|
||||
native: 'Como está o irmão mais velho?',
|
||||
explanation: '"Kumusta na" significa "Como está", "ang" é um artigo, "Kuya" significa "irmão mais velho"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo ra siya.',
|
||||
native: 'Ele está bem.',
|
||||
explanation: '"Maayo ra" significa "bem", "siya" significa "ele"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gutom na ko, Nanay.',
|
||||
native: 'Estou com fome, Mãe.',
|
||||
explanation: '"Gutom" significa "fome", "na" mostra um estado, "ko" é "eu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Hulata lang, hapit na ang pagkaon.',
|
||||
native: 'Apenas espere, a comida está quase pronta.',
|
||||
explanation: '"Hulata" significa "espere", "lang" significa "apenas", "hapit na" significa "quase", "pagkaon" significa "comida"'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
// Versuche zuerst einen System-Benutzer zu finden (z.B. mit username "system" oder "admin")
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: { [sequelize.Sequelize.Op.in]: ['system', 'admin', 'System', 'Admin'] }
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
// Erstelle einen System-Benutzer
|
||||
const password = crypto.randomBytes(32).toString('hex');
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const hashedId = crypto.createHash('sha256').update(`system-${Date.now()}`).digest('hex');
|
||||
|
||||
systemUser = await User.create({
|
||||
username: 'system',
|
||||
password: hashedPassword,
|
||||
hashedId: hashedId,
|
||||
email: 'system@your-part.de'
|
||||
});
|
||||
console.log('✅ System-Benutzer erstellt:', systemUser.hashedId);
|
||||
} else {
|
||||
console.log('✅ System-Benutzer gefunden:', systemUser.hashedId);
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
function createFamilyConversationExercises(nativeLanguageName) {
|
||||
const exercises = [];
|
||||
const conversations = FAMILY_CONVERSATIONS[nativeLanguageName]?.conversations || [];
|
||||
|
||||
if (conversations.length === 0) {
|
||||
console.warn(`⚠️ Keine Gespräche für Muttersprache "${nativeLanguageName}" gefunden. Verwende Deutsch als Fallback.`);
|
||||
return createFamilyConversationExercises('Deutsch');
|
||||
}
|
||||
|
||||
let exerciseNum = 1;
|
||||
|
||||
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche)
|
||||
conversations.forEach((conv, idx) => {
|
||||
// Erstelle für jedes Gespräch eine Multiple Choice Übung
|
||||
const wrongOptions = conversations
|
||||
.filter((c, i) => i !== idx)
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 3)
|
||||
.map(c => c.native);
|
||||
|
||||
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
|
||||
const correctIndex = options.indexOf(conv.native);
|
||||
|
||||
exercises.push({
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: `Familien-Gespräch ${idx + 1} - Übersetzung`,
|
||||
instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName,
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`,
|
||||
options: options
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: correctIndex
|
||||
}),
|
||||
explanation: conv.explanation
|
||||
});
|
||||
});
|
||||
|
||||
// Multiple Choice: Rückwärts-Übersetzung (Was bedeutet dieser Satz?)
|
||||
conversations.forEach((conv, idx) => {
|
||||
if (idx < 6) { // Erste 6 als Rückwärts-Übersetzung
|
||||
const wrongOptions = conversations
|
||||
.filter((c, i) => i !== idx)
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 3)
|
||||
.map(c => c.native);
|
||||
|
||||
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
|
||||
const correctIndex = options.indexOf(conv.native);
|
||||
|
||||
exercises.push({
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: `Familien-Gespräch ${idx + 1} - Was bedeutet dieser Satz?`,
|
||||
instruction: 'Was bedeutet dieser Bisaya-Satz?',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Was bedeutet "${conv.bisaya}"?`,
|
||||
options: options
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: correctIndex
|
||||
}),
|
||||
explanation: conv.explanation
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Gap Fill: Vervollständige Familiengespräche (mehrere Varianten)
|
||||
exercises.push({
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: 'Familien-Gespräch 1 - Vervollständigen',
|
||||
instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
text: 'Person A: Kumusta ka, {gap}? (Mama)\nPerson B: {gap} ko, Salamat. Ikaw? (Mir geht es gut)',
|
||||
gaps: 2
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
answers: ['Nanay', 'Maayo']
|
||||
}),
|
||||
explanation: '"Nanay" ist "Mama" und "Maayo ko" bedeutet "Mir geht es gut"'
|
||||
});
|
||||
|
||||
exercises.push({
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: 'Familien-Gespräch 2 - Vervollständigen',
|
||||
instruction: 'Vervollständige das Gespräch mit den richtigen Bisaya-Wörtern.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
text: 'Person A: {gap} si Tatay? (Wo ist)\nPerson B: {gap} siya sa balay. (Er ist)',
|
||||
gaps: 2
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
answers: ['Asa', 'Naa']
|
||||
}),
|
||||
explanation: '"Asa" bedeutet "wo" und "Naa" bedeutet "ist/sein"'
|
||||
});
|
||||
|
||||
// Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten)
|
||||
conversations.slice(0, 4).forEach((conv, idx) => {
|
||||
exercises.push({
|
||||
exerciseTypeId: 3, // transformation
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: `Familien-Gespräch ${idx + 1} - Übersetzung nach Bisaya`,
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'transformation',
|
||||
text: conv.native
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'transformation',
|
||||
correctAnswer: conv.bisaya
|
||||
}),
|
||||
explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}`
|
||||
});
|
||||
});
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
async function updateFamilyConversationExercises() {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
|
||||
// Finde Bisaya-Sprache mit SQL
|
||||
const [bisayaLangResult] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (!bisayaLangResult) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const bisayaLanguageId = bisayaLangResult.id;
|
||||
|
||||
// Hole alle Bisaya-Kurse mit native language info
|
||||
const courses = await sequelize.query(
|
||||
`SELECT
|
||||
c.id,
|
||||
c.title,
|
||||
c.native_language_id,
|
||||
nl.name as native_language_name
|
||||
FROM community.vocab_course c
|
||||
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
|
||||
WHERE c.language_id = :bisayaLanguageId`,
|
||||
{
|
||||
replacements: { bisayaLanguageId },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`📚 Gefunden: ${courses.length} Bisaya-Kurse\n`);
|
||||
|
||||
let totalExercisesCreated = 0;
|
||||
let totalLessonsProcessed = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📖 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
|
||||
// Finde native language name
|
||||
const nativeLanguageName = course.native_language_name || 'Deutsch';
|
||||
console.log(` Muttersprache: ${nativeLanguageName}`);
|
||||
|
||||
// Finde "Familien-Gespräche" Lektion
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: course.id,
|
||||
title: 'Familien-Gespräche'
|
||||
},
|
||||
attributes: ['id', 'title', 'lessonNumber']
|
||||
});
|
||||
|
||||
console.log(` ${lessons.length} "Familien-Gespräche"-Lektion(en) gefunden`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
// Lösche vorhandene Übungen
|
||||
const deletedCount = await VocabGrammarExercise.destroy({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
|
||||
|
||||
// Erstelle neue Übungen
|
||||
const exercises = createFamilyConversationExercises(nativeLanguageName);
|
||||
|
||||
if (exercises.length > 0) {
|
||||
const exercisesToCreate = exercises.map(ex => ({
|
||||
...ex,
|
||||
lessonId: lesson.id,
|
||||
createdByUserId: systemUser.id
|
||||
}));
|
||||
|
||||
await VocabGrammarExercise.bulkCreate(exercisesToCreate);
|
||||
totalExercisesCreated += exercisesToCreate.length;
|
||||
console.log(` ✅ ${exercisesToCreate.length} neue Übung(en) erstellt`);
|
||||
} else {
|
||||
console.log(` ⚠️ Keine Übungen erstellt`);
|
||||
}
|
||||
|
||||
totalLessonsProcessed++;
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalLessonsProcessed} "Familien-Gespräche"-Lektion(en) verarbeitet`);
|
||||
console.log(` ${totalExercisesCreated} Grammatik-Übungen erstellt`);
|
||||
}
|
||||
|
||||
updateFamilyConversationExercises()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
293
backend/scripts/update-family-words-exercises.js
Executable file
293
backend/scripts/update-family-words-exercises.js
Executable file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Aktualisieren der "Familienwörter"-Übungen in Bisaya-Kursen
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/update-family-words-exercises.js
|
||||
*
|
||||
* Ersetzt bestehende Dummy-Übungen durch spezifische Familienwörter-Übungen.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import User from '../models/community/user.js';
|
||||
|
||||
// Familienwörter-Übersetzungen in verschiedene Muttersprachen
|
||||
const FAMILY_WORDS = {
|
||||
Mutter: {
|
||||
de: 'Mutter',
|
||||
en: 'Mother',
|
||||
es: 'Madre',
|
||||
fr: 'Mère',
|
||||
it: 'Madre',
|
||||
pt: 'Mãe'
|
||||
},
|
||||
Vater: {
|
||||
de: 'Vater',
|
||||
en: 'Father',
|
||||
es: 'Padre',
|
||||
fr: 'Père',
|
||||
it: 'Padre',
|
||||
pt: 'Pai'
|
||||
},
|
||||
'älterer Bruder': {
|
||||
de: 'älterer Bruder',
|
||||
en: 'older brother',
|
||||
es: 'hermano mayor',
|
||||
fr: 'frère aîné',
|
||||
it: 'fratello maggiore',
|
||||
pt: 'irmão mais velho'
|
||||
},
|
||||
'ältere Schwester': {
|
||||
de: 'ältere Schwester',
|
||||
en: 'older sister',
|
||||
es: 'hermana mayor',
|
||||
fr: 'sœur aînée',
|
||||
it: 'sorella maggiore',
|
||||
pt: 'irmã mais velha'
|
||||
},
|
||||
Großmutter: {
|
||||
de: 'Großmutter',
|
||||
en: 'Grandmother',
|
||||
es: 'Abuela',
|
||||
fr: 'Grand-mère',
|
||||
it: 'Nonna',
|
||||
pt: 'Avó'
|
||||
},
|
||||
Großvater: {
|
||||
de: 'Großvater',
|
||||
en: 'Grandfather',
|
||||
es: 'Abuelo',
|
||||
fr: 'Grand-père',
|
||||
it: 'Nonno',
|
||||
pt: 'Avô'
|
||||
}
|
||||
};
|
||||
|
||||
// Bisaya-Übersetzungen
|
||||
const BISAYA_TRANSLATIONS = {
|
||||
'Mutter': 'Nanay',
|
||||
'Vater': 'Tatay',
|
||||
'älterer Bruder': 'Kuya',
|
||||
'ältere Schwester': 'Ate',
|
||||
'Großmutter': 'Lola',
|
||||
'Großvater': 'Lolo'
|
||||
};
|
||||
|
||||
// Sprach-Codes für Mapping
|
||||
const LANGUAGE_CODES = {
|
||||
'Deutsch': 'de',
|
||||
'English': 'en',
|
||||
'Español': 'es',
|
||||
'Français': 'fr',
|
||||
'Italiano': 'it',
|
||||
'Português': 'pt',
|
||||
'Spanish': 'es',
|
||||
'French': 'fr',
|
||||
'Italian': 'it',
|
||||
'Portuguese': 'pt'
|
||||
};
|
||||
|
||||
// Erstelle Übungen basierend auf Muttersprache
|
||||
function createFamilyWordsExercises(nativeLanguageName) {
|
||||
const langCode = LANGUAGE_CODES[nativeLanguageName] || 'de'; // Fallback zu Deutsch
|
||||
|
||||
const exercises = [];
|
||||
|
||||
// Multiple Choice Übungen für jedes Familienwort
|
||||
const familyWords = Object.keys(FAMILY_WORDS);
|
||||
const bisayaWords = ['Nanay', 'Tatay', 'Kuya', 'Ate', 'Lola', 'Lolo'];
|
||||
|
||||
familyWords.forEach((key, index) => {
|
||||
const nativeWord = FAMILY_WORDS[key][langCode];
|
||||
const bisayaWord = BISAYA_TRANSLATIONS[key];
|
||||
|
||||
// Erstelle Multiple Choice mit falschen Antworten
|
||||
const wrongAnswers = bisayaWords.filter(w => w !== bisayaWord);
|
||||
const shuffledWrong = wrongAnswers.sort(() => Math.random() - 0.5).slice(0, 3);
|
||||
const options = [bisayaWord, ...shuffledWrong].sort(() => Math.random() - 0.5);
|
||||
const correctIndex = options.indexOf(bisayaWord);
|
||||
|
||||
exercises.push({
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: `Wie sagt man "${nativeWord}" auf Bisaya?`,
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: `Wie sagt man "${nativeWord}" auf Bisaya?`,
|
||||
options: options
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: correctIndex
|
||||
},
|
||||
explanation: `"${bisayaWord}" bedeutet "${nativeWord}" auf Bisaya.`
|
||||
});
|
||||
});
|
||||
|
||||
// Gap Fill Übung
|
||||
const nativeWords = familyWords.map(key => FAMILY_WORDS[key][langCode]);
|
||||
exercises.push({
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Familienwörter vervollständigen',
|
||||
instruction: `Fülle die Lücken mit den richtigen Bisaya-Familienwörtern.`,
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: familyWords.map((key, i) => `{gap} (${nativeWords[i]})`).join(' | '),
|
||||
gaps: familyWords.length
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: bisayaWords
|
||||
},
|
||||
explanation: bisayaWords.map((bw, i) => `${bw} = ${nativeWords[i]}`).join(', ') + '.'
|
||||
});
|
||||
|
||||
// Transformation Übung
|
||||
exercises.push({
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Familienwörter übersetzen',
|
||||
instruction: `Übersetze das Familienwort ins Bisaya.`,
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: nativeWords[0], // Erste Muttersprache als Beispiel
|
||||
sourceLanguage: nativeLanguageName || 'Deutsch',
|
||||
targetLanguage: 'Bisaya'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: bisayaWords[0],
|
||||
alternatives: [bisayaWords[0]] // Nur die korrekte Antwort
|
||||
},
|
||||
explanation: `"${bisayaWords[0]}" bedeutet "${nativeWords[0]}" auf Bisaya.`
|
||||
});
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'system'
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'admin'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!systemUser) {
|
||||
console.error('❌ System-Benutzer nicht gefunden.');
|
||||
throw new Error('System user not found');
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
async function updateFamilyWordsExercises() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
|
||||
|
||||
// Finde alle Bisaya-Kurse
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
if (!bisayaLanguage) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const courses = await sequelize.query(
|
||||
`SELECT c.id, c.title, c.owner_user_id, c.native_language_id, nl.name as native_language_name
|
||||
FROM community.vocab_course c
|
||||
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
|
||||
WHERE c.language_id = :languageId`,
|
||||
{
|
||||
replacements: { languageId: bisayaLanguage.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
|
||||
|
||||
let totalExercisesUpdated = 0;
|
||||
let totalLessonsUpdated = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
const nativeLangName = course.native_language_name || 'Deutsch'; // Fallback zu Deutsch
|
||||
console.log(`📚 Kurs: ${course.title} (ID: ${course.id}, Muttersprache: ${nativeLangName})`);
|
||||
|
||||
// Erstelle Übungen für diese Muttersprache
|
||||
const exercises = createFamilyWordsExercises(nativeLangName);
|
||||
|
||||
// Finde "Familienwörter"-Lektionen
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: course.id,
|
||||
title: 'Familienwörter'
|
||||
},
|
||||
order: [['lessonNumber', 'ASC']]
|
||||
});
|
||||
|
||||
console.log(` ${lessons.length} "Familienwörter"-Lektionen gefunden\n`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
// Lösche bestehende Übungen (inkl. Dummy-Übungen)
|
||||
const deletedCount = await VocabGrammarExercise.destroy({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
|
||||
|
||||
// Erstelle neue Übungen
|
||||
let exerciseNumber = 1;
|
||||
for (const exerciseData of exercises) {
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: exerciseData.exerciseTypeId,
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: exerciseData.title,
|
||||
instruction: exerciseData.instruction,
|
||||
questionData: JSON.stringify(exerciseData.questionData),
|
||||
answerData: JSON.stringify(exerciseData.answerData),
|
||||
explanation: exerciseData.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalExercisesUpdated++;
|
||||
}
|
||||
|
||||
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} neue Übung(en) erstellt (${nativeLangName} → Bisaya)`);
|
||||
totalLessonsUpdated++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalLessonsUpdated} Lektionen aktualisiert`);
|
||||
console.log(` ${totalExercisesUpdated} neue Grammatik-Übungen erstellt`);
|
||||
}
|
||||
|
||||
updateFamilyWordsExercises()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
552
backend/scripts/update-feelings-affection-exercises.js
Executable file
552
backend/scripts/update-feelings-affection-exercises.js
Executable file
@@ -0,0 +1,552 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Erstellen von Übungen für die "Gefühle & Zuneigung" Lektion
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/update-feelings-affection-exercises.js
|
||||
*
|
||||
* Erstellt Gesprächsübungen für die "Gefühle & Zuneigung" Lektion in allen Bisaya-Kursen.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import User from '../models/community/user.js';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
// Gefühle & Zuneigung auf Bisaya mit verschiedenen Muttersprachen
|
||||
const FEELINGS_AFFECTION = {
|
||||
// Deutsch -> Bisaya
|
||||
'Deutsch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Gihigugma ko ikaw.',
|
||||
native: 'Ich liebe dich.',
|
||||
explanation: '"Gihigugma" bedeutet "lieben", "ko" ist "ich", "ikaw" ist "dich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nahigugma ko nimo.',
|
||||
native: 'Ich liebe dich. (alternativ)',
|
||||
explanation: '"Nahigugma" ist eine andere Form von "lieben", "nimo" ist "dich" (informell)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Ganahan ko nimo.',
|
||||
native: 'Ich mag dich.',
|
||||
explanation: '"Ganahan" bedeutet "mögen/gefallen", "ko" ist "ich", "nimo" ist "dich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga nakita ka.',
|
||||
native: 'Ich bin glücklich, dich zu sehen.',
|
||||
explanation: '"Nalipay" bedeutet "glücklich", "ko" ist "ich", "nga nakita ka" ist "dich zu sehen"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gimingaw ko nimo.',
|
||||
native: 'Ich vermisse dich.',
|
||||
explanation: '"Gimingaw" bedeutet "vermissen", "ko" ist "ich", "nimo" ist "dich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko.',
|
||||
native: 'Ich bin glücklich.',
|
||||
explanation: '"Nalipay" bedeutet "glücklich", "ko" ist "ich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nasubo ko.',
|
||||
native: 'Ich bin traurig.',
|
||||
explanation: '"Nasubo" bedeutet "traurig", "ko" ist "ich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga naa ka dinhi.',
|
||||
native: 'Ich bin glücklich, dass du hier bist.',
|
||||
explanation: '"Nalipay" ist "glücklich", "nga naa ka dinhi" bedeutet "dass du hier bist"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Englisch -> Bisaya
|
||||
'Englisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Gihigugma ko ikaw.',
|
||||
native: 'I love you.',
|
||||
explanation: '"Gihigugma" means "love", "ko" is "I", "ikaw" is "you"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nahigugma ko nimo.',
|
||||
native: 'I love you. (alternative)',
|
||||
explanation: '"Nahigugma" is another form of "love", "nimo" is "you" (informal)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Ganahan ko nimo.',
|
||||
native: 'I like you.',
|
||||
explanation: '"Ganahan" means "like", "ko" is "I", "nimo" is "you"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga nakita ka.',
|
||||
native: 'I am happy to see you.',
|
||||
explanation: '"Nalipay" means "happy", "ko" is "I", "nga nakita ka" is "to see you"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gimingaw ko nimo.',
|
||||
native: 'I miss you.',
|
||||
explanation: '"Gimingaw" means "miss", "ko" is "I", "nimo" is "you"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko.',
|
||||
native: 'I am happy.',
|
||||
explanation: '"Nalipay" means "happy", "ko" is "I"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nasubo ko.',
|
||||
native: 'I am sad.',
|
||||
explanation: '"Nasubo" means "sad", "ko" is "I"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga naa ka dinhi.',
|
||||
native: 'I am happy that you are here.',
|
||||
explanation: '"Nalipay" is "happy", "nga naa ka dinhi" means "that you are here"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Spanisch -> Bisaya
|
||||
'Spanisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Gihigugma ko ikaw.',
|
||||
native: 'Te amo.',
|
||||
explanation: '"Gihigugma" significa "amar", "ko" es "yo", "ikaw" es "tú"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nahigugma ko nimo.',
|
||||
native: 'Te amo. (alternativa)',
|
||||
explanation: '"Nahigugma" es otra forma de "amar", "nimo" es "tú" (informal)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Ganahan ko nimo.',
|
||||
native: 'Me gustas.',
|
||||
explanation: '"Ganahan" significa "gustar", "ko" es "yo", "nimo" es "tú"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga nakita ka.',
|
||||
native: 'Estoy feliz de verte.',
|
||||
explanation: '"Nalipay" significa "feliz", "ko" es "yo", "nga nakita ka" es "verte"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gimingaw ko nimo.',
|
||||
native: 'Te extraño.',
|
||||
explanation: '"Gimingaw" significa "extrañar", "ko" es "yo", "nimo" es "tú"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko.',
|
||||
native: 'Estoy feliz.',
|
||||
explanation: '"Nalipay" significa "feliz", "ko" es "yo"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nasubo ko.',
|
||||
native: 'Estoy triste.',
|
||||
explanation: '"Nasubo" significa "triste", "ko" es "yo"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga naa ka dinhi.',
|
||||
native: 'Estoy feliz de que estés aquí.',
|
||||
explanation: '"Nalipay" es "feliz", "nga naa ka dinhi" significa "que estés aquí"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Französisch -> Bisaya
|
||||
'Französisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Gihigugma ko ikaw.',
|
||||
native: 'Je t\'aime.',
|
||||
explanation: '"Gihigugma" signifie "aimer", "ko" est "je", "ikaw" est "tu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nahigugma ko nimo.',
|
||||
native: 'Je t\'aime. (alternative)',
|
||||
explanation: '"Nahigugma" est une autre forme de "aimer", "nimo" est "tu" (informel)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Ganahan ko nimo.',
|
||||
native: 'Je t\'aime bien.',
|
||||
explanation: '"Ganahan" signifie "aimer bien", "ko" est "je", "nimo" est "tu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga nakita ka.',
|
||||
native: 'Je suis heureux de te voir.',
|
||||
explanation: '"Nalipay" signifie "heureux", "ko" est "je", "nga nakita ka" est "te voir"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gimingaw ko nimo.',
|
||||
native: 'Tu me manques.',
|
||||
explanation: '"Gimingaw" signifie "manquer", "ko" est "je", "nimo" est "tu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko.',
|
||||
native: 'Je suis heureux.',
|
||||
explanation: '"Nalipay" signifie "heureux", "ko" est "je"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nasubo ko.',
|
||||
native: 'Je suis triste.',
|
||||
explanation: '"Nasubo" signifie "triste", "ko" est "je"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga naa ka dinhi.',
|
||||
native: 'Je suis heureux que tu sois ici.',
|
||||
explanation: '"Nalipay" est "heureux", "nga naa ka dinhi" signifie "que tu sois ici"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Italienisch -> Bisaya
|
||||
'Italienisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Gihigugma ko ikaw.',
|
||||
native: 'Ti amo.',
|
||||
explanation: '"Gihigugma" significa "amare", "ko" è "io", "ikaw" è "tu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nahigugma ko nimo.',
|
||||
native: 'Ti amo. (alternativa)',
|
||||
explanation: '"Nahigugma" è un\'altra forma di "amare", "nimo" è "tu" (informale)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Ganahan ko nimo.',
|
||||
native: 'Mi piaci.',
|
||||
explanation: '"Ganahan" significa "piacere", "ko" è "io", "nimo" è "tu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga nakita ka.',
|
||||
native: 'Sono felice di vederti.',
|
||||
explanation: '"Nalipay" significa "felice", "ko" è "io", "nga nakita ka" è "vederti"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gimingaw ko nimo.',
|
||||
native: 'Mi manchi.',
|
||||
explanation: '"Gimingaw" significa "mancare", "ko" è "io", "nimo" è "tu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko.',
|
||||
native: 'Sono felice.',
|
||||
explanation: '"Nalipay" significa "felice", "ko" è "io"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nasubo ko.',
|
||||
native: 'Sono triste.',
|
||||
explanation: '"Nasubo" significa "triste", "ko" è "io"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga naa ka dinhi.',
|
||||
native: 'Sono felice che tu sia qui.',
|
||||
explanation: '"Nalipay" è "felice", "nga naa ka dinhi" significa "che tu sia qui"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Portugiesisch -> Bisaya
|
||||
'Portugiesisch': {
|
||||
conversations: [
|
||||
{
|
||||
bisaya: 'Gihigugma ko ikaw.',
|
||||
native: 'Eu te amo.',
|
||||
explanation: '"Gihigugma" significa "amar", "ko" é "eu", "ikaw" é "você"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nahigugma ko nimo.',
|
||||
native: 'Eu te amo. (alternativa)',
|
||||
explanation: '"Nahigugma" é outra forma de "amar", "nimo" é "você" (informal)'
|
||||
},
|
||||
{
|
||||
bisaya: 'Ganahan ko nimo.',
|
||||
native: 'Eu gosto de você.',
|
||||
explanation: '"Ganahan" significa "gostar", "ko" é "eu", "nimo" é "você"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga nakita ka.',
|
||||
native: 'Estou feliz em te ver.',
|
||||
explanation: '"Nalipay" significa "feliz", "ko" é "eu", "nga nakita ka" é "te ver"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gimingaw ko nimo.',
|
||||
native: 'Eu sinto sua falta.',
|
||||
explanation: '"Gimingaw" significa "sentir falta", "ko" é "eu", "nimo" é "você"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko.',
|
||||
native: 'Estou feliz.',
|
||||
explanation: '"Nalipay" significa "feliz", "ko" é "eu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nasubo ko.',
|
||||
native: 'Estou triste.',
|
||||
explanation: '"Nasubo" significa "triste", "ko" é "eu"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Nalipay ko nga naa ka dinhi.',
|
||||
native: 'Estou feliz que você esteja aqui.',
|
||||
explanation: '"Nalipay" é "feliz", "nga naa ka dinhi" significa "que você esteja aqui"'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
// Versuche zuerst einen System-Benutzer zu finden (z.B. mit username "system" oder "admin")
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: { [sequelize.Sequelize.Op.in]: ['system', 'admin', 'System', 'Admin'] }
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
// Erstelle einen System-Benutzer
|
||||
const password = crypto.randomBytes(32).toString('hex');
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const hashedId = crypto.createHash('sha256').update(`system-${Date.now()}`).digest('hex');
|
||||
|
||||
systemUser = await User.create({
|
||||
username: 'system',
|
||||
password: hashedPassword,
|
||||
hashedId: hashedId,
|
||||
email: 'system@your-part.de'
|
||||
});
|
||||
console.log('✅ System-Benutzer erstellt:', systemUser.hashedId);
|
||||
} else {
|
||||
console.log('✅ System-Benutzer gefunden:', systemUser.hashedId);
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
function createFeelingsAffectionExercises(nativeLanguageName) {
|
||||
const exercises = [];
|
||||
const conversations = FEELINGS_AFFECTION[nativeLanguageName]?.conversations || [];
|
||||
|
||||
if (conversations.length === 0) {
|
||||
console.warn(`⚠️ Keine Gespräche für Muttersprache "${nativeLanguageName}" gefunden. Verwende Deutsch als Fallback.`);
|
||||
return createFeelingsAffectionExercises('Deutsch');
|
||||
}
|
||||
|
||||
let exerciseNum = 1;
|
||||
|
||||
// Multiple Choice: Übersetze Bisaya-Satz in Muttersprache (alle Gespräche)
|
||||
conversations.forEach((conv, idx) => {
|
||||
// Erstelle für jedes Gespräch eine Multiple Choice Übung
|
||||
const wrongOptions = conversations
|
||||
.filter((c, i) => i !== idx)
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 3)
|
||||
.map(c => c.native);
|
||||
|
||||
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
|
||||
const correctIndex = options.indexOf(conv.native);
|
||||
|
||||
exercises.push({
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung`,
|
||||
instruction: 'Übersetze den Bisaya-Satz ins ' + nativeLanguageName,
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Wie sagt man "${conv.bisaya}" auf ${nativeLanguageName}?`,
|
||||
options: options
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: correctIndex
|
||||
}),
|
||||
explanation: conv.explanation
|
||||
});
|
||||
});
|
||||
|
||||
// Multiple Choice: Rückwärts-Übersetzung (Was bedeutet dieser Satz?)
|
||||
conversations.forEach((conv, idx) => {
|
||||
if (idx < 6) { // Erste 6 als Rückwärts-Übersetzung
|
||||
const wrongOptions = conversations
|
||||
.filter((c, i) => i !== idx)
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 3)
|
||||
.map(c => c.native);
|
||||
|
||||
const options = [conv.native, ...wrongOptions].sort(() => Math.random() - 0.5);
|
||||
const correctIndex = options.indexOf(conv.native);
|
||||
|
||||
exercises.push({
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: `Gefühle & Zuneigung ${idx + 1} - Was bedeutet dieser Satz?`,
|
||||
instruction: 'Was bedeutet dieser Bisaya-Satz?',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Was bedeutet "${conv.bisaya}"?`,
|
||||
options: options
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: correctIndex
|
||||
}),
|
||||
explanation: conv.explanation
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Gap Fill: Vervollständige Gefühlsausdrücke (mehrere Varianten)
|
||||
exercises.push({
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: 'Gefühle & Zuneigung 1 - Vervollständigen',
|
||||
instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
text: 'Person A: {gap} ko ikaw. (Ich liebe dich)\nPerson B: {gap} ko pud. (Ich liebe dich auch)',
|
||||
gaps: 2
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
answers: ['Gihigugma', 'Gihigugma']
|
||||
}),
|
||||
explanation: '"Gihigugma" bedeutet "lieben" und wird wiederholt, um "auch" auszudrücken'
|
||||
});
|
||||
|
||||
exercises.push({
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: 'Gefühle & Zuneigung 2 - Vervollständigen',
|
||||
instruction: 'Vervollständige den Satz mit den richtigen Bisaya-Wörtern.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
text: 'Person A: {gap} ko nga nakita ka. (Ich bin glücklich)\nPerson B: {gap} ko pud. (Ich auch)',
|
||||
gaps: 2
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'gap_fill',
|
||||
answers: ['Nalipay', 'Nalipay']
|
||||
}),
|
||||
explanation: '"Nalipay" bedeutet "glücklich sein"'
|
||||
});
|
||||
|
||||
// Transformation: Übersetze Muttersprache-Satz nach Bisaya (mehrere Varianten)
|
||||
conversations.slice(0, 4).forEach((conv, idx) => {
|
||||
exercises.push({
|
||||
exerciseTypeId: 3, // transformation
|
||||
exerciseNumber: exerciseNum++,
|
||||
title: `Gefühle & Zuneigung ${idx + 1} - Übersetzung nach Bisaya`,
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'transformation',
|
||||
text: conv.native
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'transformation',
|
||||
correctAnswer: conv.bisaya
|
||||
}),
|
||||
explanation: `"${conv.bisaya}" bedeutet "${conv.native}" auf Bisaya. ${conv.explanation}`
|
||||
});
|
||||
});
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
async function updateFeelingsAffectionExercises() {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
|
||||
// Finde Bisaya-Sprache mit SQL
|
||||
const [bisayaLangResult] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (!bisayaLangResult) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const bisayaLanguageId = bisayaLangResult.id;
|
||||
|
||||
// Hole alle Bisaya-Kurse mit native language info
|
||||
const courses = await sequelize.query(
|
||||
`SELECT
|
||||
c.id,
|
||||
c.title,
|
||||
c.native_language_id,
|
||||
nl.name as native_language_name
|
||||
FROM community.vocab_course c
|
||||
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
|
||||
WHERE c.language_id = :bisayaLanguageId`,
|
||||
{
|
||||
replacements: { bisayaLanguageId },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`📚 Gefunden: ${courses.length} Bisaya-Kurse\n`);
|
||||
|
||||
let totalExercisesCreated = 0;
|
||||
let totalLessonsProcessed = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📖 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
|
||||
// Finde native language name
|
||||
const nativeLanguageName = course.native_language_name || 'Deutsch';
|
||||
console.log(` Muttersprache: ${nativeLanguageName}`);
|
||||
|
||||
// Finde "Gefühle & Zuneigung" Lektion
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: course.id,
|
||||
title: 'Gefühle & Zuneigung'
|
||||
},
|
||||
attributes: ['id', 'title', 'lessonNumber']
|
||||
});
|
||||
|
||||
console.log(` ${lessons.length} "Gefühle & Zuneigung"-Lektion(en) gefunden`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
// Lösche vorhandene Übungen
|
||||
const deletedCount = await VocabGrammarExercise.destroy({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
|
||||
|
||||
// Erstelle neue Übungen
|
||||
const exercises = createFeelingsAffectionExercises(nativeLanguageName);
|
||||
|
||||
if (exercises.length > 0) {
|
||||
const exercisesToCreate = exercises.map(ex => ({
|
||||
...ex,
|
||||
lessonId: lesson.id,
|
||||
createdByUserId: systemUser.id
|
||||
}));
|
||||
|
||||
await VocabGrammarExercise.bulkCreate(exercisesToCreate);
|
||||
totalExercisesCreated += exercisesToCreate.length;
|
||||
console.log(` ✅ ${exercisesToCreate.length} neue Übung(en) erstellt`);
|
||||
} else {
|
||||
console.log(` ⚠️ Keine Übungen erstellt`);
|
||||
}
|
||||
|
||||
totalLessonsProcessed++;
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalLessonsProcessed} "Gefühle & Zuneigung"-Lektion(en) verarbeitet`);
|
||||
console.log(` ${totalExercisesCreated} Grammatik-Übungen erstellt`);
|
||||
}
|
||||
|
||||
updateFeelingsAffectionExercises()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
730
backend/scripts/update-food-care-exercises.js
Executable file
730
backend/scripts/update-food-care-exercises.js
Executable file
@@ -0,0 +1,730 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Erstellen von Übungen für die "Essen & Fürsorge" und "Essen & Trinken" Lektionen
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/update-food-care-exercises.js
|
||||
*
|
||||
* Erstellt Gesprächsübungen für die "Essen & Fürsorge" und "Essen & Trinken" Lektionen in allen Bisaya-Kursen.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import VocabCourse from '../models/community/vocab_course.js';
|
||||
import User from '../models/community/user.js';
|
||||
import crypto from 'crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
// Essen & Fürsorge / Essen & Trinken auf Bisaya mit verschiedenen Muttersprachen
|
||||
const FOOD_CARE_CONVERSATIONS = {
|
||||
// Deutsch -> Bisaya
|
||||
'Deutsch': {
|
||||
'Essen & Fürsorge': [
|
||||
{
|
||||
bisaya: 'Gutom na ko.',
|
||||
native: 'Ich habe Hunger.',
|
||||
explanation: '"Gutom" bedeutet "Hunger", "na" ist "schon", "ko" ist "ich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gihikap ko.',
|
||||
native: 'Ich habe Durst.',
|
||||
explanation: '"Gihikap" bedeutet "Durst haben", "ko" ist "ich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gusto ka mokaon?',
|
||||
native: 'Möchtest du essen?',
|
||||
explanation: '"Gusto" bedeutet "möchten", "ka" ist "du", "mokaon" ist "essen"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Oo, gusto ko.',
|
||||
native: 'Ja, ich möchte.',
|
||||
explanation: '"Oo" ist "Ja", "gusto ko" ist "ich möchte"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Unsa ang gusto nimo?',
|
||||
native: 'Was möchtest du?',
|
||||
explanation: '"Unsa" ist "Was", "ang" ist Artikel, "gusto nimo" ist "du möchtest"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gusto ko ug kan-on.',
|
||||
native: 'Ich möchte Reis.',
|
||||
explanation: '"Gusto ko" ist "ich möchte", "ug" ist "und/ein", "kan-on" ist "Reis"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Palihug, hatagi ko ug tubig.',
|
||||
native: 'Bitte gib mir Wasser.',
|
||||
explanation: '"Palihug" ist "Bitte", "hatagi" ist "geben", "ko" ist "mir", "ug tubig" ist "Wasser"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Salamat sa pagkaon.',
|
||||
native: 'Danke für das Essen.',
|
||||
explanation: '"Salamat" ist "Danke", "sa pagkaon" ist "für das Essen"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Lami kaayo!',
|
||||
native: 'Sehr lecker!',
|
||||
explanation: '"Lami" bedeutet "lecker", "kaayo" ist "sehr"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Busog na ko.',
|
||||
native: 'Ich bin satt.',
|
||||
explanation: '"Busog" bedeutet "satt", "na" ist "schon", "ko" ist "ich"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta ang pagkaon?',
|
||||
native: 'Wie schmeckt das Essen?',
|
||||
explanation: '"Kumusta" ist "Wie", "ang pagkaon" ist "das Essen"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo kaayo ang pagkaon.',
|
||||
native: 'Das Essen ist sehr gut.',
|
||||
explanation: '"Maayo" ist "gut", "kaayo" ist "sehr", "ang pagkaon" ist "das Essen"'
|
||||
}
|
||||
],
|
||||
'Essen & Trinken': [
|
||||
{
|
||||
bisaya: 'Kan-on',
|
||||
native: 'Reis',
|
||||
explanation: '"Kan-on" ist das grundlegende Wort für "Reis"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Tubig',
|
||||
native: 'Wasser',
|
||||
explanation: '"Tubig" bedeutet "Wasser"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Pan',
|
||||
native: 'Brot',
|
||||
explanation: '"Pan" bedeutet "Brot"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Isda',
|
||||
native: 'Fisch',
|
||||
explanation: '"Isda" bedeutet "Fisch"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Manok',
|
||||
native: 'Huhn',
|
||||
explanation: '"Manok" bedeutet "Huhn"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Baboy',
|
||||
native: 'Schwein',
|
||||
explanation: '"Baboy" bedeutet "Schwein"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gulay',
|
||||
native: 'Gemüse',
|
||||
explanation: '"Gulay" bedeutet "Gemüse"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Prutas',
|
||||
native: 'Obst',
|
||||
explanation: '"Prutas" bedeutet "Obst"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gatas',
|
||||
native: 'Milch',
|
||||
explanation: '"Gatas" bedeutet "Milch"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kape',
|
||||
native: 'Kaffee',
|
||||
explanation: '"Kape" bedeutet "Kaffee"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Tsa',
|
||||
native: 'Tee',
|
||||
explanation: '"Tsa" bedeutet "Tee"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asin',
|
||||
native: 'Salz',
|
||||
explanation: '"Asin" bedeutet "Salz"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asukar',
|
||||
native: 'Zucker',
|
||||
explanation: '"Asukar" bedeutet "Zucker"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Tinapay',
|
||||
native: 'Brot (alternativ)',
|
||||
explanation: '"Tinapay" ist eine alternative Bezeichnung für "Brot"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Bugas',
|
||||
native: 'Reis (roh)',
|
||||
explanation: '"Bugas" ist ungekochter Reis, "kan-on" ist gekochter Reis'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Englisch -> Bisaya
|
||||
'Englisch': {
|
||||
'Essen & Fürsorge': [
|
||||
{
|
||||
bisaya: 'Gutom na ko.',
|
||||
native: 'I am hungry.',
|
||||
explanation: '"Gutom" means "hungry", "na" is "already", "ko" is "I"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gihikap ko.',
|
||||
native: 'I am thirsty.',
|
||||
explanation: '"Gihikap" means "thirsty", "ko" is "I"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gusto ka mokaon?',
|
||||
native: 'Do you want to eat?',
|
||||
explanation: '"Gusto" means "want", "ka" is "you", "mokaon" is "to eat"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Oo, gusto ko.',
|
||||
native: 'Yes, I want.',
|
||||
explanation: '"Oo" is "Yes", "gusto ko" is "I want"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Unsa ang gusto nimo?',
|
||||
native: 'What do you want?',
|
||||
explanation: '"Unsa" is "What", "ang" is article, "gusto nimo" is "you want"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gusto ko ug kan-on.',
|
||||
native: 'I want rice.',
|
||||
explanation: '"Gusto ko" is "I want", "ug" is "a/some", "kan-on" is "rice"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Palihug, hatagi ko ug tubig.',
|
||||
native: 'Please give me water.',
|
||||
explanation: '"Palihug" is "Please", "hatagi" is "give", "ko" is "me", "ug tubig" is "water"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Salamat sa pagkaon.',
|
||||
native: 'Thank you for the food.',
|
||||
explanation: '"Salamat" is "Thank you", "sa pagkaon" is "for the food"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Lami kaayo!',
|
||||
native: 'Very delicious!',
|
||||
explanation: '"Lami" means "delicious", "kaayo" is "very"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Busog na ko.',
|
||||
native: 'I am full.',
|
||||
explanation: '"Busog" means "full", "na" is "already", "ko" is "I"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kumusta ang pagkaon?',
|
||||
native: 'How is the food?',
|
||||
explanation: '"Kumusta" is "How", "ang pagkaon" is "the food"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Maayo kaayo ang pagkaon.',
|
||||
native: 'The food is very good.',
|
||||
explanation: '"Maayo" is "good", "kaayo" is "very", "ang pagkaon" is "the food"'
|
||||
}
|
||||
],
|
||||
'Essen & Trinken': [
|
||||
{
|
||||
bisaya: 'Kan-on',
|
||||
native: 'Rice',
|
||||
explanation: '"Kan-on" is the basic word for "rice"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Tubig',
|
||||
native: 'Water',
|
||||
explanation: '"Tubig" means "water"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Pan',
|
||||
native: 'Bread',
|
||||
explanation: '"Pan" means "bread"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Isda',
|
||||
native: 'Fish',
|
||||
explanation: '"Isda" means "fish"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Manok',
|
||||
native: 'Chicken',
|
||||
explanation: '"Manok" means "chicken"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Baboy',
|
||||
native: 'Pig',
|
||||
explanation: '"Baboy" means "pig"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gulay',
|
||||
native: 'Vegetables',
|
||||
explanation: '"Gulay" means "vegetables"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Prutas',
|
||||
native: 'Fruit',
|
||||
explanation: '"Prutas" means "fruit"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Gatas',
|
||||
native: 'Milk',
|
||||
explanation: '"Gatas" means "milk"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Kape',
|
||||
native: 'Coffee',
|
||||
explanation: '"Kape" means "coffee"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Tsa',
|
||||
native: 'Tea',
|
||||
explanation: '"Tsa" means "tea"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asin',
|
||||
native: 'Salt',
|
||||
explanation: '"Asin" means "salt"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Asukar',
|
||||
native: 'Sugar',
|
||||
explanation: '"Asukar" means "sugar"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Tinapay',
|
||||
native: 'Bread (alternative)',
|
||||
explanation: '"Tinapay" is an alternative term for "bread"'
|
||||
},
|
||||
{
|
||||
bisaya: 'Bugas',
|
||||
native: 'Rice (uncooked)',
|
||||
explanation: '"Bugas" is uncooked rice, "kan-on" is cooked rice'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Erweitere für weitere Sprachen (Spanisch, Französisch, Italienisch, Portugiesisch, Tagalog)
|
||||
const ADDITIONAL_LANGUAGES = {
|
||||
'Spanisch': {
|
||||
'Essen & Fürsorge': [
|
||||
{ bisaya: 'Gutom na ko.', native: 'Tengo hambre.', explanation: '"Gutom" significa "hambre", "na" es "ya", "ko" es "yo"' },
|
||||
{ bisaya: 'Gihikap ko.', native: 'Tengo sed.', explanation: '"Gihikap" significa "sed", "ko" es "yo"' },
|
||||
{ bisaya: 'Gusto ka mokaon?', native: '¿Quieres comer?', explanation: '"Gusto" significa "querer", "ka" es "tú", "mokaon" es "comer"' },
|
||||
{ bisaya: 'Oo, gusto ko.', native: 'Sí, quiero.', explanation: '"Oo" es "Sí", "gusto ko" es "quiero"' },
|
||||
{ bisaya: 'Unsa ang gusto nimo?', native: '¿Qué quieres?', explanation: '"Unsa" es "Qué", "ang" es artículo, "gusto nimo" es "quieres"' },
|
||||
{ bisaya: 'Gusto ko ug kan-on.', native: 'Quiero arroz.', explanation: '"Gusto ko" es "quiero", "ug kan-on" es "arroz"' },
|
||||
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Por favor, dame agua.', explanation: '"Palihug" es "Por favor", "hatagi" es "dar", "ko" es "me", "ug tubig" es "agua"' },
|
||||
{ bisaya: 'Salamat sa pagkaon.', native: 'Gracias por la comida.', explanation: '"Salamat" es "Gracias", "sa pagkaon" es "por la comida"' },
|
||||
{ bisaya: 'Lami kaayo!', native: '¡Muy delicioso!', explanation: '"Lami" significa "delicioso", "kaayo" es "muy"' },
|
||||
{ bisaya: 'Busog na ko.', native: 'Estoy lleno.', explanation: '"Busog" significa "lleno", "na" es "ya", "ko" es "yo"' },
|
||||
{ bisaya: 'Kumusta ang pagkaon?', native: '¿Cómo está la comida?', explanation: '"Kumusta" es "Cómo", "ang pagkaon" es "la comida"' },
|
||||
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'La comida está muy buena.', explanation: '"Maayo" es "buena", "kaayo" es "muy", "ang pagkaon" es "la comida"' }
|
||||
],
|
||||
'Essen & Trinken': [
|
||||
{ bisaya: 'Kan-on', native: 'Arroz', explanation: '"Kan-on" es la palabra básica para "arroz"' },
|
||||
{ bisaya: 'Tubig', native: 'Agua', explanation: '"Tubig" significa "agua"' },
|
||||
{ bisaya: 'Pan', native: 'Pan', explanation: '"Pan" significa "pan"' },
|
||||
{ bisaya: 'Isda', native: 'Pescado', explanation: '"Isda" significa "pescado"' },
|
||||
{ bisaya: 'Manok', native: 'Pollo', explanation: '"Manok" significa "pollo"' },
|
||||
{ bisaya: 'Baboy', native: 'Cerdo', explanation: '"Baboy" significa "cerdo"' },
|
||||
{ bisaya: 'Gulay', native: 'Verduras', explanation: '"Gulay" significa "verduras"' },
|
||||
{ bisaya: 'Prutas', native: 'Fruta', explanation: '"Prutas" significa "fruta"' },
|
||||
{ bisaya: 'Gatas', native: 'Leche', explanation: '"Gatas" significa "leche"' },
|
||||
{ bisaya: 'Kape', native: 'Café', explanation: '"Kape" significa "café"' },
|
||||
{ bisaya: 'Tsa', native: 'Té', explanation: '"Tsa" significa "té"' },
|
||||
{ bisaya: 'Asin', native: 'Sal', explanation: '"Asin" significa "sal"' },
|
||||
{ bisaya: 'Asukar', native: 'Azúcar', explanation: '"Asukar" significa "azúcar"' },
|
||||
{ bisaya: 'Tinapay', native: 'Pan (alternativo)', explanation: '"Tinapay" es un término alternativo para "pan"' },
|
||||
{ bisaya: 'Bugas', native: 'Arroz (crudo)', explanation: '"Bugas" es arroz crudo, "kan-on" es arroz cocido' }
|
||||
]
|
||||
},
|
||||
'Französisch': {
|
||||
'Essen & Fürsorge': [
|
||||
{ bisaya: 'Gutom na ko.', native: 'J\'ai faim.', explanation: '"Gutom" signifie "faim", "na" est "déjà", "ko" est "je"' },
|
||||
{ bisaya: 'Gihikap ko.', native: 'J\'ai soif.', explanation: '"Gihikap" signifie "soif", "ko" est "je"' },
|
||||
{ bisaya: 'Gusto ka mokaon?', native: 'Tu veux manger?', explanation: '"Gusto" signifie "vouloir", "ka" est "tu", "mokaon" est "manger"' },
|
||||
{ bisaya: 'Oo, gusto ko.', native: 'Oui, je veux.', explanation: '"Oo" est "Oui", "gusto ko" est "je veux"' },
|
||||
{ bisaya: 'Unsa ang gusto nimo?', native: 'Que veux-tu?', explanation: '"Unsa" est "Que", "ang" est article, "gusto nimo" est "tu veux"' },
|
||||
{ bisaya: 'Gusto ko ug kan-on.', native: 'Je veux du riz.', explanation: '"Gusto ko" est "je veux", "ug kan-on" est "riz"' },
|
||||
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'S\'il te plaît, donne-moi de l\'eau.', explanation: '"Palihug" est "S\'il te plaît", "hatagi" est "donner", "ko" est "moi", "ug tubig" est "eau"' },
|
||||
{ bisaya: 'Salamat sa pagkaon.', native: 'Merci pour la nourriture.', explanation: '"Salamat" est "Merci", "sa pagkaon" est "pour la nourriture"' },
|
||||
{ bisaya: 'Lami kaayo!', native: 'Très délicieux!', explanation: '"Lami" signifie "délicieux", "kaayo" est "très"' },
|
||||
{ bisaya: 'Busog na ko.', native: 'Je suis rassasié.', explanation: '"Busog" signifie "rassasié", "na" est "déjà", "ko" est "je"' },
|
||||
{ bisaya: 'Kumusta ang pagkaon?', native: 'Comment est la nourriture?', explanation: '"Kumusta" est "Comment", "ang pagkaon" est "la nourriture"' },
|
||||
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'La nourriture est très bonne.', explanation: '"Maayo" est "bonne", "kaayo" est "très", "ang pagkaon" est "la nourriture"' }
|
||||
],
|
||||
'Essen & Trinken': [
|
||||
{ bisaya: 'Kan-on', native: 'Riz', explanation: '"Kan-on" est le mot de base pour "riz"' },
|
||||
{ bisaya: 'Tubig', native: 'Eau', explanation: '"Tubig" signifie "eau"' },
|
||||
{ bisaya: 'Pan', native: 'Pain', explanation: '"Pan" signifie "pain"' },
|
||||
{ bisaya: 'Isda', native: 'Poisson', explanation: '"Isda" signifie "poisson"' },
|
||||
{ bisaya: 'Manok', native: 'Poulet', explanation: '"Manok" signifie "poulet"' },
|
||||
{ bisaya: 'Baboy', native: 'Porc', explanation: '"Baboy" signifie "porc"' },
|
||||
{ bisaya: 'Gulay', native: 'Légumes', explanation: '"Gulay" signifie "légumes"' },
|
||||
{ bisaya: 'Prutas', native: 'Fruit', explanation: '"Prutas" signifie "fruit"' },
|
||||
{ bisaya: 'Gatas', native: 'Lait', explanation: '"Gatas" signifie "lait"' },
|
||||
{ bisaya: 'Kape', native: 'Café', explanation: '"Kape" signifie "café"' },
|
||||
{ bisaya: 'Tsa', native: 'Thé', explanation: '"Tsa" signifie "thé"' },
|
||||
{ bisaya: 'Asin', native: 'Sel', explanation: '"Asin" signifie "sel"' },
|
||||
{ bisaya: 'Asukar', native: 'Sucre', explanation: '"Asukar" signifie "sucre"' },
|
||||
{ bisaya: 'Tinapay', native: 'Pain (alternatif)', explanation: '"Tinapay" est un terme alternatif pour "pain"' },
|
||||
{ bisaya: 'Bugas', native: 'Riz (cru)', explanation: '"Bugas" est riz cru, "kan-on" est riz cuit' }
|
||||
]
|
||||
},
|
||||
'Italienisch': {
|
||||
'Essen & Fürsorge': [
|
||||
{ bisaya: 'Gutom na ko.', native: 'Ho fame.', explanation: '"Gutom" significa "fame", "na" è "già", "ko" è "io"' },
|
||||
{ bisaya: 'Gihikap ko.', native: 'Ho sete.', explanation: '"Gihikap" significa "sete", "ko" è "io"' },
|
||||
{ bisaya: 'Gusto ka mokaon?', native: 'Vuoi mangiare?', explanation: '"Gusto" significa "volere", "ka" è "tu", "mokaon" è "mangiare"' },
|
||||
{ bisaya: 'Oo, gusto ko.', native: 'Sì, voglio.', explanation: '"Oo" è "Sì", "gusto ko" è "voglio"' },
|
||||
{ bisaya: 'Unsa ang gusto nimo?', native: 'Cosa vuoi?', explanation: '"Unsa" è "Cosa", "ang" è articolo, "gusto nimo" è "vuoi"' },
|
||||
{ bisaya: 'Gusto ko ug kan-on.', native: 'Voglio riso.', explanation: '"Gusto ko" è "voglio", "ug kan-on" è "riso"' },
|
||||
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Per favore, dammi acqua.', explanation: '"Palihug" è "Per favore", "hatagi" è "dare", "ko" è "mi", "ug tubig" è "acqua"' },
|
||||
{ bisaya: 'Salamat sa pagkaon.', native: 'Grazie per il cibo.', explanation: '"Salamat" è "Grazie", "sa pagkaon" è "per il cibo"' },
|
||||
{ bisaya: 'Lami kaayo!', native: 'Molto delizioso!', explanation: '"Lami" significa "delizioso", "kaayo" è "molto"' },
|
||||
{ bisaya: 'Busog na ko.', native: 'Sono sazio.', explanation: '"Busog" significa "sazio", "na" è "già", "ko" è "io"' },
|
||||
{ bisaya: 'Kumusta ang pagkaon?', native: 'Com\'è il cibo?', explanation: '"Kumusta" è "Come", "ang pagkaon" è "il cibo"' },
|
||||
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'Il cibo è molto buono.', explanation: '"Maayo" è "buono", "kaayo" è "molto", "ang pagkaon" è "il cibo"' }
|
||||
],
|
||||
'Essen & Trinken': [
|
||||
{ bisaya: 'Kan-on', native: 'Riso', explanation: '"Kan-on" è la parola base per "riso"' },
|
||||
{ bisaya: 'Tubig', native: 'Acqua', explanation: '"Tubig" significa "acqua"' },
|
||||
{ bisaya: 'Pan', native: 'Pane', explanation: '"Pan" significa "pane"' },
|
||||
{ bisaya: 'Isda', native: 'Pesce', explanation: '"Isda" significa "pesce"' },
|
||||
{ bisaya: 'Manok', native: 'Pollo', explanation: '"Manok" significa "pollo"' },
|
||||
{ bisaya: 'Baboy', native: 'Maiale', explanation: '"Baboy" significa "maiale"' },
|
||||
{ bisaya: 'Gulay', native: 'Verdura', explanation: '"Gulay" significa "verdura"' },
|
||||
{ bisaya: 'Prutas', native: 'Frutta', explanation: '"Prutas" significa "frutta"' },
|
||||
{ bisaya: 'Gatas', native: 'Latte', explanation: '"Gatas" significa "latte"' },
|
||||
{ bisaya: 'Kape', native: 'Caffè', explanation: '"Kape" significa "caffè"' },
|
||||
{ bisaya: 'Tsa', native: 'Tè', explanation: '"Tsa" significa "tè"' },
|
||||
{ bisaya: 'Asin', native: 'Sale', explanation: '"Asin" significa "sale"' },
|
||||
{ bisaya: 'Asukar', native: 'Zucchero', explanation: '"Asukar" significa "zucchero"' },
|
||||
{ bisaya: 'Tinapay', native: 'Pane (alternativo)', explanation: '"Tinapay" è un termine alternativo per "pane"' },
|
||||
{ bisaya: 'Bugas', native: 'Riso (crudo)', explanation: '"Bugas" è riso crudo, "kan-on" è riso cotto' }
|
||||
]
|
||||
},
|
||||
'Portugiesisch': {
|
||||
'Essen & Fürsorge': [
|
||||
{ bisaya: 'Gutom na ko.', native: 'Tenho fome.', explanation: '"Gutom" significa "fome", "na" é "já", "ko" é "eu"' },
|
||||
{ bisaya: 'Gihikap ko.', native: 'Tenho sede.', explanation: '"Gihikap" significa "sede", "ko" é "eu"' },
|
||||
{ bisaya: 'Gusto ka mokaon?', native: 'Quer comer?', explanation: '"Gusto" significa "querer", "ka" é "você", "mokaon" é "comer"' },
|
||||
{ bisaya: 'Oo, gusto ko.', native: 'Sim, quero.', explanation: '"Oo" é "Sim", "gusto ko" é "quero"' },
|
||||
{ bisaya: 'Unsa ang gusto nimo?', native: 'O que você quer?', explanation: '"Unsa" é "O que", "ang" é artigo, "gusto nimo" é "você quer"' },
|
||||
{ bisaya: 'Gusto ko ug kan-on.', native: 'Quero arroz.', explanation: '"Gusto ko" é "quero", "ug kan-on" é "arroz"' },
|
||||
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Por favor, me dê água.', explanation: '"Palihug" é "Por favor", "hatagi" é "dar", "ko" é "me", "ug tubig" é "água"' },
|
||||
{ bisaya: 'Salamat sa pagkaon.', native: 'Obrigado pela comida.', explanation: '"Salamat" é "Obrigado", "sa pagkaon" é "pela comida"' },
|
||||
{ bisaya: 'Lami kaayo!', native: 'Muito delicioso!', explanation: '"Lami" significa "delicioso", "kaayo" é "muito"' },
|
||||
{ bisaya: 'Busog na ko.', native: 'Estou cheio.', explanation: '"Busog" significa "cheio", "na" é "já", "ko" é "eu"' },
|
||||
{ bisaya: 'Kumusta ang pagkaon?', native: 'Como está a comida?', explanation: '"Kumusta" é "Como", "ang pagkaon" é "a comida"' },
|
||||
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'A comida está muito boa.', explanation: '"Maayo" é "boa", "kaayo" é "muito", "ang pagkaon" é "a comida"' }
|
||||
],
|
||||
'Essen & Trinken': [
|
||||
{ bisaya: 'Kan-on', native: 'Arroz', explanation: '"Kan-on" é a palavra básica para "arroz"' },
|
||||
{ bisaya: 'Tubig', native: 'Água', explanation: '"Tubig" significa "água"' },
|
||||
{ bisaya: 'Pan', native: 'Pão', explanation: '"Pan" significa "pão"' },
|
||||
{ bisaya: 'Isda', native: 'Peixe', explanation: '"Isda" significa "peixe"' },
|
||||
{ bisaya: 'Manok', native: 'Frango', explanation: '"Manok" significa "frango"' },
|
||||
{ bisaya: 'Baboy', native: 'Porco', explanation: '"Baboy" significa "porco"' },
|
||||
{ bisaya: 'Gulay', native: 'Legumes', explanation: '"Gulay" significa "legumes"' },
|
||||
{ bisaya: 'Prutas', native: 'Fruta', explanation: '"Prutas" significa "fruta"' },
|
||||
{ bisaya: 'Gatas', native: 'Leite', explanation: '"Gatas" significa "leite"' },
|
||||
{ bisaya: 'Kape', native: 'Café', explanation: '"Kape" significa "café"' },
|
||||
{ bisaya: 'Tsa', native: 'Chá', explanation: '"Tsa" significa "chá"' },
|
||||
{ bisaya: 'Asin', native: 'Sal', explanation: '"Asin" significa "sal"' },
|
||||
{ bisaya: 'Asukar', native: 'Açúcar', explanation: '"Asukar" significa "açúcar"' },
|
||||
{ bisaya: 'Tinapay', native: 'Pão (alternativo)', explanation: '"Tinapay" é um termo alternativo para "pão"' },
|
||||
{ bisaya: 'Bugas', native: 'Arroz (cru)', explanation: '"Bugas" é arroz cru, "kan-on" é arroz cozido' }
|
||||
]
|
||||
},
|
||||
'Tagalog': {
|
||||
'Essen & Fürsorge': [
|
||||
{ bisaya: 'Gutom na ko.', native: 'Gutom na ako.', explanation: '"Gutom" ay "gutom", "na" ay "na", "ko" ay "ako"' },
|
||||
{ bisaya: 'Gihikap ko.', native: 'Nauuhaw ako.', explanation: '"Gihikap" ay "nauuhaw", "ko" ay "ako"' },
|
||||
{ bisaya: 'Gusto ka mokaon?', native: 'Gusto mo bang kumain?', explanation: '"Gusto" ay "gusto", "ka" ay "mo", "mokaon" ay "kumain"' },
|
||||
{ bisaya: 'Oo, gusto ko.', native: 'Oo, gusto ko.', explanation: '"Oo" ay "Oo", "gusto ko" ay "gusto ko"' },
|
||||
{ bisaya: 'Unsa ang gusto nimo?', native: 'Ano ang gusto mo?', explanation: '"Unsa" ay "Ano", "ang" ay "ang", "gusto nimo" ay "gusto mo"' },
|
||||
{ bisaya: 'Gusto ko ug kan-on.', native: 'Gusto ko ng kanin.', explanation: '"Gusto ko" ay "gusto ko", "ug kan-on" ay "kanin"' },
|
||||
{ bisaya: 'Palihug, hatagi ko ug tubig.', native: 'Pakiusap, bigyan mo ako ng tubig.', explanation: '"Palihug" ay "Pakiusap", "hatagi" ay "bigyan", "ko" ay "ako", "ug tubig" ay "tubig"' },
|
||||
{ bisaya: 'Salamat sa pagkaon.', native: 'Salamat sa pagkain.', explanation: '"Salamat" ay "Salamat", "sa pagkaon" ay "sa pagkain"' },
|
||||
{ bisaya: 'Lami kaayo!', native: 'Masarap talaga!', explanation: '"Lami" ay "masarap", "kaayo" ay "talaga"' },
|
||||
{ bisaya: 'Busog na ko.', native: 'Busog na ako.', explanation: '"Busog" ay "busog", "na" ay "na", "ko" ay "ako"' },
|
||||
{ bisaya: 'Kumusta ang pagkaon?', native: 'Kumusta ang pagkain?', explanation: '"Kumusta" ay "Kumusta", "ang pagkaon" ay "ang pagkain"' },
|
||||
{ bisaya: 'Maayo kaayo ang pagkaon.', native: 'Mabuti talaga ang pagkain.', explanation: '"Maayo" ay "mabuti", "kaayo" ay "talaga", "ang pagkaon" ay "ang pagkain"' }
|
||||
],
|
||||
'Essen & Trinken': [
|
||||
{ bisaya: 'Kan-on', native: 'Kanin', explanation: '"Kan-on" ay ang salitang base para sa "kanin"' },
|
||||
{ bisaya: 'Tubig', native: 'Tubig', explanation: '"Tubig" ay "tubig"' },
|
||||
{ bisaya: 'Pan', native: 'Tinapay', explanation: '"Pan" ay "tinapay"' },
|
||||
{ bisaya: 'Isda', native: 'Isda', explanation: '"Isda" ay "isda"' },
|
||||
{ bisaya: 'Manok', native: 'Manok', explanation: '"Manok" ay "manok"' },
|
||||
{ bisaya: 'Baboy', native: 'Baboy', explanation: '"Baboy" ay "baboy"' },
|
||||
{ bisaya: 'Gulay', native: 'Gulay', explanation: '"Gulay" ay "gulay"' },
|
||||
{ bisaya: 'Prutas', native: 'Prutas', explanation: '"Prutas" ay "prutas"' },
|
||||
{ bisaya: 'Gatas', native: 'Gatas', explanation: '"Gatas" ay "gatas"' },
|
||||
{ bisaya: 'Kape', native: 'Kape', explanation: '"Kape" ay "kape"' },
|
||||
{ bisaya: 'Tsa', native: 'Tsa', explanation: '"Tsa" ay "tsa"' },
|
||||
{ bisaya: 'Asin', native: 'Asin', explanation: '"Asin" ay "asin"' },
|
||||
{ bisaya: 'Asukar', native: 'Asukal', explanation: '"Asukar" ay "asukal"' },
|
||||
{ bisaya: 'Tinapay', native: 'Tinapay', explanation: '"Tinapay" ay "tinapay"' },
|
||||
{ bisaya: 'Bugas', native: 'Bigas', explanation: '"Bugas" ay "bigas", "kan-on" ay "kanin"' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Kombiniere alle Sprachen
|
||||
const ALL_FOOD_CARE = { ...FOOD_CARE_CONVERSATIONS, ...ADDITIONAL_LANGUAGES };
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: { [Op.in]: ['system', 'admin', 'System', 'Admin'] }
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
console.error('❌ System-Benutzer nicht gefunden.');
|
||||
throw new Error('System user not found');
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
async function updateFoodCareExercises() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
|
||||
|
||||
// Finde alle Bisaya-Kurse
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
if (!bisayaLanguage) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const bisayaLanguageId = bisayaLanguage.id;
|
||||
|
||||
// Hole alle Bisaya-Kurse mit native language info
|
||||
const courses = await sequelize.query(
|
||||
`SELECT
|
||||
c.id,
|
||||
c.title,
|
||||
c.owner_user_id,
|
||||
c.native_language_id,
|
||||
nl.name as native_language_name
|
||||
FROM community.vocab_course c
|
||||
LEFT JOIN community.vocab_language nl ON c.native_language_id = nl.id
|
||||
WHERE c.language_id = :bisayaLanguageId`,
|
||||
{
|
||||
replacements: { bisayaLanguageId },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
|
||||
|
||||
let totalExercisesCreated = 0;
|
||||
let totalLessonsUpdated = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
const nativeLangName = course.native_language_name || 'Deutsch';
|
||||
console.log(` Muttersprache: ${nativeLangName}`);
|
||||
|
||||
// Finde "Essen & Fürsorge" und "Essen & Trinken" Lektionen
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: course.id,
|
||||
title: ['Essen & Fürsorge', 'Essen & Trinken']
|
||||
},
|
||||
order: [['lessonNumber', 'ASC']]
|
||||
});
|
||||
|
||||
console.log(` ${lessons.length} Lektion(en) gefunden\n`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
const conversations = ALL_FOOD_CARE[nativeLangName]?.[lesson.title];
|
||||
|
||||
if (!conversations || conversations.length === 0) {
|
||||
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen für Muttersprache "${nativeLangName}" definiert`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lösche bestehende Übungen
|
||||
const deletedCount = await VocabGrammarExercise.destroy({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
|
||||
|
||||
// Erstelle neue Übungen basierend auf dem Lektionstyp
|
||||
let exerciseNumber = 1;
|
||||
|
||||
if (lesson.title === 'Essen & Fürsorge') {
|
||||
// Gesprächsübungen für "Essen & Fürsorge"
|
||||
for (const conv of conversations) {
|
||||
// Multiple Choice: Übersetzung von Muttersprache zu Bisaya
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: `Wie sagt man "${conv.native}"?`,
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Wie sagt man "${conv.native}" auf Bisaya?`,
|
||||
options: [
|
||||
conv.bisaya,
|
||||
conversations[(exerciseNumber - 2 + 1) % conversations.length]?.bisaya || 'Salamat',
|
||||
conversations[(exerciseNumber - 2 + 2) % conversations.length]?.bisaya || 'Maayo',
|
||||
conversations[(exerciseNumber - 2 + 3) % conversations.length]?.bisaya || 'Palihug'
|
||||
]
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
}),
|
||||
explanation: conv.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalExercisesCreated++;
|
||||
|
||||
// Multiple Choice: Übersetzung von Bisaya zu Muttersprache
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: `Was bedeutet "${conv.bisaya}"?`,
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Was bedeutet "${conv.bisaya}"?`,
|
||||
options: [
|
||||
conv.native,
|
||||
conversations[(exerciseNumber - 3 + 1) % conversations.length]?.native || 'Danke',
|
||||
conversations[(exerciseNumber - 3 + 2) % conversations.length]?.native || 'Bitte',
|
||||
conversations[(exerciseNumber - 3 + 3) % conversations.length]?.native || 'Gut'
|
||||
]
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
}),
|
||||
explanation: conv.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalExercisesCreated++;
|
||||
}
|
||||
|
||||
// Transformation-Übungen
|
||||
const selectedConvs = conversations.slice(0, 3);
|
||||
for (const conv of selectedConvs) {
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: 4, // transformation
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: `Übersetze: "${conv.native}"`,
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'transformation',
|
||||
text: conv.native,
|
||||
sourceLanguage: nativeLangName,
|
||||
targetLanguage: 'Bisaya'
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'transformation',
|
||||
correct: conv.bisaya,
|
||||
alternatives: []
|
||||
}),
|
||||
explanation: conv.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalExercisesCreated++;
|
||||
}
|
||||
} else if (lesson.title === 'Essen & Trinken') {
|
||||
// Vokabular-Übungen für "Essen & Trinken"
|
||||
for (const vocab of conversations) {
|
||||
// Multiple Choice: Muttersprache -> Bisaya
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: `Wie sagt man "${vocab.native}"?`,
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Wie sagt man "${vocab.native}" auf Bisaya?`,
|
||||
options: [
|
||||
vocab.bisaya,
|
||||
conversations[(exerciseNumber - 2 + 1) % conversations.length]?.bisaya || 'Salamat',
|
||||
conversations[(exerciseNumber - 2 + 2) % conversations.length]?.bisaya || 'Maayo',
|
||||
conversations[(exerciseNumber - 2 + 3) % conversations.length]?.bisaya || 'Palihug'
|
||||
]
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
}),
|
||||
explanation: vocab.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalExercisesCreated++;
|
||||
|
||||
// Multiple Choice: Bisaya -> Muttersprache
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: `Was bedeutet "${vocab.bisaya}"?`,
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
question: `Was bedeutet "${vocab.bisaya}"?`,
|
||||
options: [
|
||||
vocab.native,
|
||||
conversations[(exerciseNumber - 3 + 1) % conversations.length]?.native || 'Danke',
|
||||
conversations[(exerciseNumber - 3 + 2) % conversations.length]?.native || 'Bitte',
|
||||
conversations[(exerciseNumber - 3 + 3) % conversations.length]?.native || 'Gut'
|
||||
]
|
||||
}),
|
||||
answerData: JSON.stringify({
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
}),
|
||||
explanation: vocab.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalExercisesCreated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` ✅ ${lessons.length} Lektion(en) aktualisiert\n`);
|
||||
totalLessonsUpdated += lessons.length;
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalLessonsUpdated} Lektionen aktualisiert`);
|
||||
console.log(` ${totalExercisesCreated} neue Grammatik-Übungen erstellt`);
|
||||
}
|
||||
|
||||
updateFoodCareExercises()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
454
backend/scripts/update-survival-sentences-exercises.js
Executable file
454
backend/scripts/update-survival-sentences-exercises.js
Executable file
@@ -0,0 +1,454 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Aktualisieren der "Überlebenssätze"-Übungen in Bisaya-Kursen
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/update-survival-sentences-exercises.js
|
||||
*
|
||||
* Ersetzt bestehende generische Übungen durch spezifische Überlebenssätze-Übungen.
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import User from '../models/community/user.js';
|
||||
|
||||
// Spezifische Übungen für Überlebenssätze
|
||||
const SURVIVAL_EXERCISES = {
|
||||
'Überlebenssätze - Teil 1': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Ich verstehe nicht"?',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
|
||||
options: ['Wala ko kasabot', 'Palihug', 'Salamat', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht" - sehr wichtig für Anfänger!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Kannst du das wiederholen?"?',
|
||||
instruction: 'Wähle die richtige Bitte aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Kannst du das wiederholen?" auf Bisaya?',
|
||||
options: ['Palihug ka mubalik?', 'Salamat', 'Maayo', 'Kumusta ka?']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?" - essentiell für das Lernen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Wo ist...?"?',
|
||||
instruction: 'Wähle die richtige Frage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wo ist die Toilette?" auf Bisaya?',
|
||||
options: ['Asa ang CR?', 'Kumusta ka?', 'Salamat', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Asa ang CR?" bedeutet "Wo ist die Toilette?" - "Asa" = "Wo", "CR" = "Comfort Room" (Toilette).'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Vervollständige den Satz: "Ich verstehe nicht"',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?',
|
||||
options: ['Wala ko kasabot', 'Dili ko kasabot', 'Wala ko makasabot', 'Dili ko makasabot']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Vervollständige den Satz: "Bitte wiederholen"',
|
||||
instruction: 'Wähle die richtige Übersetzung.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Bitte wiederholen" auf Bisaya?',
|
||||
options: ['Palihug ka mubalik?', 'Palihug balik', 'Salamat mubalik', 'Maayo mubalik']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Palihug ka mubalik?" bedeutet "Bitte kannst du wiederholen?".'
|
||||
}
|
||||
],
|
||||
|
||||
'Überlebenssätze - Teil 2': [
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Wie viel kostet das?"?',
|
||||
instruction: 'Wähle die richtige Frage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Wie viel kostet das?" auf Bisaya?',
|
||||
options: ['Tagpila ni?', 'Asa ni?', 'Unsa ni?', 'Kinsa ni?']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Entschuldigung"?',
|
||||
instruction: 'Wähle die richtige Entschuldigung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Entschuldigung" auf Bisaya?',
|
||||
options: ['Pasensya', 'Salamat', 'Palihug', 'Maayo']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Was ist das?"?',
|
||||
instruction: 'Wähle die richtige Frage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Was ist das?" auf Bisaya?',
|
||||
options: ['Unsa ni?', 'Asa ni?', 'Tagpila ni?', 'Kinsa ni?']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Unsa ni?" bedeutet "Was ist das?" - "Unsa" = "Was", "ni" = "das".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Bitte langsam"?',
|
||||
instruction: 'Wähle die richtige Bitte aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Bitte langsam" auf Bisaya?',
|
||||
options: ['Hinay-hinay lang', 'Palihug lang', 'Maayo lang', 'Salamat lang']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Hinay-hinay lang" bedeutet "Bitte langsam" - sehr wichtig, wenn jemand zu schnell spricht!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Ich spreche kein Bisaya"?',
|
||||
instruction: 'Wähle die richtige Aussage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Ich spreche kein Bisaya" auf Bisaya?',
|
||||
options: ['Dili ko mag-Bisaya', 'Wala ko mag-Bisaya', 'Maayo ko mag-Bisaya', 'Salamat ko mag-Bisaya']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Kannst du das aufschreiben?"?',
|
||||
instruction: 'Wähle die richtige Bitte aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Kannst du das aufschreiben?" auf Bisaya?',
|
||||
options: ['Palihug isulat ni', 'Palihug basahon ni', 'Palihug sulaton ni', 'Palihug pakigamit ni']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Palihug isulat ni" bedeutet "Bitte schreibe das auf" - hilfreich beim Lernen neuer Wörter.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Wie sagt man "Ich bin verloren"?',
|
||||
instruction: 'Wähle die richtige Aussage aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Wie sagt man "Ich bin verloren" auf Bisaya?',
|
||||
options: ['Nawala ko', 'Naa ko', 'Maayo ko', 'Salamat ko']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Nawala ko" bedeutet "Ich bin verloren" - wichtig, wenn du Hilfe brauchst.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Wichtige Fragen bilden',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Fragewörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: '{gap} ni? (Wie viel kostet das?) | {gap} ni? (Was ist das?) | {gap} lang (Bitte langsam)',
|
||||
gaps: 3
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['Tagpila', 'Unsa', 'Hinay-hinay']
|
||||
},
|
||||
explanation: '"Tagpila" = "Wie viel", "Unsa" = "Was", "Hinay-hinay lang" = "Bitte langsam".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 1, // gap_fill
|
||||
title: 'Überlebenssätze vervollständigen',
|
||||
instruction: 'Fülle die Lücken mit den richtigen Wörtern.',
|
||||
questionData: {
|
||||
type: 'gap_fill',
|
||||
text: 'Palihug {gap} ni (Bitte schreibe das auf) | {gap} ko (Ich bin verloren) | Dili ko {gap} (Ich spreche kein Bisaya)',
|
||||
gaps: 3
|
||||
},
|
||||
answerData: {
|
||||
type: 'gap_fill',
|
||||
answers: ['isulat', 'Nawala', 'mag-Bisaya']
|
||||
},
|
||||
explanation: '"isulat" = "aufschreiben", "Nawala" = "verloren", "mag-Bisaya" = "Bisaya sprechen".'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Was bedeutet "Tagpila"?',
|
||||
instruction: 'Wähle die richtige Bedeutung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Tagpila"?',
|
||||
options: ['Wie viel', 'Was', 'Wo', 'Wer']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Tagpila" bedeutet "Wie viel" und wird verwendet, um nach Preisen zu fragen.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Was bedeutet "Pasensya"?',
|
||||
instruction: 'Wähle die richtige Bedeutung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Pasensya"?',
|
||||
options: ['Entschuldigung', 'Danke', 'Bitte', 'Gut']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Pasensya" bedeutet "Entschuldigung" oder "Entschuldige bitte" - wichtig für höfliche Kommunikation.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 2, // multiple_choice
|
||||
title: 'Was bedeutet "Hinay-hinay lang"?',
|
||||
instruction: 'Wähle die richtige Bedeutung aus.',
|
||||
questionData: {
|
||||
type: 'multiple_choice',
|
||||
question: 'Was bedeutet "Hinay-hinay lang"?',
|
||||
options: ['Bitte langsam', 'Bitte schnell', 'Bitte laut', 'Bitte leise']
|
||||
},
|
||||
answerData: {
|
||||
type: 'multiple_choice',
|
||||
correctAnswer: 0
|
||||
},
|
||||
explanation: '"Hinay-hinay lang" bedeutet "Bitte langsam" - sehr wichtig, wenn jemand zu schnell spricht!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Überlebenssätze übersetzen - Einkaufen',
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Wie viel kostet das?',
|
||||
sourceLanguage: 'Deutsch',
|
||||
targetLanguage: 'Bisaya'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Tagpila ni?',
|
||||
alternatives: ['Tagpila kini?', 'Pila ni?']
|
||||
},
|
||||
explanation: '"Tagpila ni?" bedeutet "Wie viel kostet das?" - sehr nützlich beim Einkaufen!'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Überlebenssätze übersetzen - Kommunikation',
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Ich spreche kein Bisaya',
|
||||
sourceLanguage: 'Deutsch',
|
||||
targetLanguage: 'Bisaya'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Dili ko mag-Bisaya',
|
||||
alternatives: ['Wala ko mag-Bisaya', 'Dili ko makasabot Bisaya']
|
||||
},
|
||||
explanation: '"Dili ko mag-Bisaya" bedeutet "Ich spreche kein Bisaya" - nützlich, um zu erklären, dass du noch lernst.'
|
||||
},
|
||||
{
|
||||
exerciseTypeId: 4, // transformation
|
||||
title: 'Überlebenssätze übersetzen - Hilfe',
|
||||
instruction: 'Übersetze den Satz ins Bisaya.',
|
||||
questionData: {
|
||||
type: 'transformation',
|
||||
text: 'Ich bin verloren',
|
||||
sourceLanguage: 'Deutsch',
|
||||
targetLanguage: 'Bisaya'
|
||||
},
|
||||
answerData: {
|
||||
type: 'transformation',
|
||||
correct: 'Nawala ko',
|
||||
alternatives: ['Nawala ako', 'Nawala na ko']
|
||||
},
|
||||
explanation: '"Nawala ko" bedeutet "Ich bin verloren" - wichtig, wenn du Hilfe brauchst.'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
async function findOrCreateSystemUser() {
|
||||
let systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'system'
|
||||
}
|
||||
});
|
||||
|
||||
if (!systemUser) {
|
||||
systemUser = await User.findOne({
|
||||
where: {
|
||||
username: 'admin'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!systemUser) {
|
||||
console.error('❌ System-Benutzer nicht gefunden.');
|
||||
throw new Error('System user not found');
|
||||
}
|
||||
|
||||
return systemUser;
|
||||
}
|
||||
|
||||
async function updateSurvivalExercises() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
const systemUser = await findOrCreateSystemUser();
|
||||
console.log(`Verwende System-Benutzer: ${systemUser.username} (ID: ${systemUser.id})\n`);
|
||||
|
||||
// Finde alle Bisaya-Kurse
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
if (!bisayaLanguage) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const courses = await sequelize.query(
|
||||
`SELECT id, title, owner_user_id FROM community.vocab_course WHERE language_id = :languageId`,
|
||||
{
|
||||
replacements: { languageId: bisayaLanguage.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${courses.length} Bisaya-Kurse\n`);
|
||||
|
||||
let totalExercisesUpdated = 0;
|
||||
let totalLessonsUpdated = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
|
||||
// Finde "Überlebenssätze"-Lektionen
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: {
|
||||
courseId: course.id,
|
||||
title: ['Überlebenssätze - Teil 1', 'Überlebenssätze - Teil 2']
|
||||
},
|
||||
order: [['lessonNumber', 'ASC']]
|
||||
});
|
||||
|
||||
console.log(` ${lessons.length} "Überlebenssätze"-Lektionen gefunden\n`);
|
||||
|
||||
for (const lesson of lessons) {
|
||||
const exercises = SURVIVAL_EXERCISES[lesson.title];
|
||||
|
||||
if (!exercises || exercises.length === 0) {
|
||||
console.log(` ⚠️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - keine Übungen definiert`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lösche bestehende Übungen
|
||||
const deletedCount = await VocabGrammarExercise.destroy({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) gelöscht`);
|
||||
|
||||
// Erstelle neue Übungen
|
||||
let exerciseNumber = 1;
|
||||
for (const exerciseData of exercises) {
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: exerciseData.exerciseTypeId,
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: exerciseData.title,
|
||||
instruction: exerciseData.instruction,
|
||||
questionData: JSON.stringify(exerciseData.questionData),
|
||||
answerData: JSON.stringify(exerciseData.answerData),
|
||||
explanation: exerciseData.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalExercisesUpdated++;
|
||||
}
|
||||
|
||||
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} neue Übung(en) erstellt`);
|
||||
totalLessonsUpdated++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalLessonsUpdated} Lektionen aktualisiert`);
|
||||
console.log(` ${totalExercisesUpdated} neue Grammatik-Übungen erstellt`);
|
||||
}
|
||||
|
||||
updateSurvivalExercises()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
129
backend/scripts/update-week1-bisaya-exercises.js
Normal file
129
backend/scripts/update-week1-bisaya-exercises.js
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script zum Aktualisieren der Woche-1-Lektionen in Bisaya-Kursen
|
||||
*
|
||||
* Verwendung:
|
||||
* node backend/scripts/update-week1-bisaya-exercises.js
|
||||
*
|
||||
* - Entfernt alte Platzhalter-Übungen
|
||||
* - Ersetzt durch korrekte Inhalte für "Woche 1 - Wiederholung" und "Woche 1 - Vokabeltest"
|
||||
*/
|
||||
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
|
||||
import User from '../models/community/user.js';
|
||||
|
||||
const LESSON_TITLES = ['Woche 1 - Wiederholung', 'Woche 1 - Vokabeltest'];
|
||||
|
||||
const BISAYA_EXERCISES = {
|
||||
'Woche 1 - Wiederholung': [
|
||||
{ exerciseTypeId: 2, title: 'Wiederholung: Wie sagt man "Wie geht es dir?"?', instruction: 'Wähle die richtige Begrüßung aus.', questionData: { type: 'multiple_choice', question: 'Wie sagt man "Wie geht es dir?" auf Bisaya?', options: ['Kumusta ka?', 'Maayo', 'Salamat', 'Palihug'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Kumusta ka?" ist die Standard-Begrüßung auf Bisaya.' },
|
||||
{ exerciseTypeId: 2, title: 'Wiederholung: Wie sagt man "Mutter" auf Bisaya?', instruction: 'Wähle die richtige Übersetzung.', questionData: { type: 'multiple_choice', question: 'Wie sagt man "Mutter" auf Bisaya?', options: ['Nanay', 'Tatay', 'Kuya', 'Ate'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Nanay" bedeutet "Mutter" auf Bisaya.' },
|
||||
{ exerciseTypeId: 2, title: 'Wiederholung: Was bedeutet "Palangga taka"?', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Palangga taka"?', options: ['Ich hab dich lieb', 'Danke', 'Guten Tag', 'Auf Wiedersehen'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Palangga taka" bedeutet "Ich hab dich lieb" - wärmer als "I love you" im Familienkontext.' },
|
||||
{ exerciseTypeId: 2, title: 'Wiederholung: Was fragt man mit "Nikaon ka?"?', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Nikaon ka?"?', options: ['Hast du schon gegessen?', 'Wie geht es dir?', 'Danke', 'Bitte'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Nikaon ka?" bedeutet "Hast du schon gegessen?" - typisch fürsorglich auf den Philippinen.' },
|
||||
{ exerciseTypeId: 2, title: 'Wiederholung: Wie sagt man "Ich verstehe nicht"?', instruction: 'Wähle die richtige Übersetzung.', questionData: { type: 'multiple_choice', question: 'Wie sagt man "Ich verstehe nicht" auf Bisaya?', options: ['Wala ko kasabot', 'Salamat', 'Maayo', 'Palihug'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Wala ko kasabot" bedeutet "Ich verstehe nicht".' }
|
||||
],
|
||||
'Woche 1 - Vokabeltest': [
|
||||
{ exerciseTypeId: 2, title: 'Vokabeltest: Kumusta', instruction: 'Was bedeutet "Kumusta"?', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Kumusta"?', options: ['Wie geht es dir?', 'Danke', 'Bitte', 'Auf Wiedersehen'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Kumusta" kommt von spanisch "¿Cómo está?" - "Wie geht es dir?"' },
|
||||
{ exerciseTypeId: 2, title: 'Vokabeltest: Lola', instruction: 'Wähle die richtige Übersetzung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Lola"?', options: ['Großmutter', 'Großvater', 'Mutter', 'Vater'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Lola" = Großmutter, "Lolo" = Großvater.' },
|
||||
{ exerciseTypeId: 2, title: 'Vokabeltest: Salamat', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Salamat"?', options: ['Danke', 'Bitte', 'Entschuldigung', 'Gern geschehen'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Salamat" bedeutet "Danke".' },
|
||||
{ exerciseTypeId: 2, title: 'Vokabeltest: Lami', instruction: 'Was bedeutet "Lami"?', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Lami"?', options: ['Lecker', 'Viel', 'Gut', 'Schnell'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Lami" bedeutet "lecker" oder "schmackhaft" - wichtig beim Essen!' },
|
||||
{ exerciseTypeId: 2, title: 'Vokabeltest: Mingaw ko nimo', instruction: 'Wähle die richtige Bedeutung.', questionData: { type: 'multiple_choice', question: 'Was bedeutet "Mingaw ko nimo"?', options: ['Ich vermisse dich', 'Ich freue mich', 'Ich mag dich', 'Ich liebe dich'] }, answerData: { type: 'multiple_choice', correctAnswer: 0 }, explanation: '"Mingaw ko nimo" bedeutet "Ich vermisse dich".' }
|
||||
]
|
||||
};
|
||||
|
||||
async function updateWeek1BisayaExercises() {
|
||||
await sequelize.authenticate();
|
||||
console.log('Datenbankverbindung erfolgreich hergestellt.\n');
|
||||
|
||||
let systemUser;
|
||||
try {
|
||||
systemUser = await User.findOne({ where: { username: 'system' } });
|
||||
if (!systemUser) systemUser = await User.findOne({ where: { username: 'admin' } });
|
||||
if (!systemUser) throw new Error('System user not found');
|
||||
} catch (e) {
|
||||
console.error('❌ System-Benutzer nicht gefunden.');
|
||||
throw e;
|
||||
}
|
||||
|
||||
const [bisayaLanguage] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE name = 'Bisaya' LIMIT 1`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (!bisayaLanguage) {
|
||||
console.error('❌ Bisaya-Sprache nicht gefunden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const courses = await sequelize.query(
|
||||
`SELECT c.id, c.title, c.owner_user_id
|
||||
FROM community.vocab_course c
|
||||
WHERE c.language_id = :languageId`,
|
||||
{
|
||||
replacements: { languageId: bisayaLanguage.id },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`Gefunden: ${courses.length} Bisaya-Kurs(e)\n`);
|
||||
|
||||
let totalDeleted = 0;
|
||||
let totalAdded = 0;
|
||||
|
||||
for (const course of courses) {
|
||||
console.log(`📚 Kurs: ${course.title} (ID: ${course.id})`);
|
||||
|
||||
for (const lessonTitle of LESSON_TITLES) {
|
||||
const exercises = BISAYA_EXERCISES[lessonTitle];
|
||||
if (!exercises || exercises.length === 0) continue;
|
||||
|
||||
const lessons = await VocabCourseLesson.findAll({
|
||||
where: { courseId: course.id, title: lessonTitle },
|
||||
order: [['lessonNumber', 'ASC']]
|
||||
});
|
||||
|
||||
for (const lesson of lessons) {
|
||||
const deletedCount = await VocabGrammarExercise.destroy({
|
||||
where: { lessonId: lesson.id }
|
||||
});
|
||||
totalDeleted += deletedCount;
|
||||
console.log(` 🗑️ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${deletedCount} alte Übung(en) entfernt`);
|
||||
|
||||
let exerciseNumber = 1;
|
||||
for (const ex of exercises) {
|
||||
await VocabGrammarExercise.create({
|
||||
lessonId: lesson.id,
|
||||
exerciseTypeId: ex.exerciseTypeId,
|
||||
exerciseNumber: exerciseNumber++,
|
||||
title: ex.title,
|
||||
instruction: ex.instruction,
|
||||
questionData: JSON.stringify(ex.questionData),
|
||||
answerData: JSON.stringify(ex.answerData),
|
||||
explanation: ex.explanation,
|
||||
createdByUserId: course.owner_user_id || systemUser.id
|
||||
});
|
||||
totalAdded++;
|
||||
}
|
||||
console.log(` ✅ Lektion ${lesson.lessonNumber}: "${lesson.title}" - ${exercises.length} neue Übung(en)`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Zusammenfassung:`);
|
||||
console.log(` ${totalDeleted} Platzhalter-Übungen entfernt`);
|
||||
console.log(` ${totalAdded} neue Übungen erstellt`);
|
||||
}
|
||||
|
||||
updateWeek1BisayaExercises()
|
||||
.then(() => {
|
||||
sequelize.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Fehler:', error);
|
||||
sequelize.close();
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,19 +1,60 @@
|
||||
import './config/loadEnv.js'; // .env deterministisch laden
|
||||
|
||||
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 { setupWebSocket } from './utils/socket.js';
|
||||
import { syncDatabase } from './utils/syncDatabase.js';
|
||||
|
||||
const server = http.createServer(app);
|
||||
// HTTP-Server für API (Port 2020, intern, über Apache-Proxy)
|
||||
const API_PORT = Number.parseInt(process.env.PORT || '2020', 10);
|
||||
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
|
||||
|
||||
setupWebSocket(server);
|
||||
// HTTPS-Server für Socket.io (Port 4443, direkt erreichbar)
|
||||
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;
|
||||
|
||||
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(() => {
|
||||
const port = process.env.PORT || 3001;
|
||||
server.listen(port, () => {
|
||||
console.log('Server is running on port', port);
|
||||
// API-Server auf Port 2020 (intern, nur localhost)
|
||||
httpServer.listen(API_PORT, '127.0.0.1', () => {
|
||||
console.log(`[API] HTTP-Server läuft auf localhost:${API_PORT} (intern, über Apache-Proxy)`);
|
||||
});
|
||||
|
||||
// Socket.io-Server auf Port 4443 (extern, direkt erreichbar)
|
||||
if (httpsServer) {
|
||||
httpsServer.listen(SOCKET_IO_PORT, '0.0.0.0', () => {
|
||||
console.log(`[Socket.io] HTTPS-Server läuft auf Port ${SOCKET_IO_PORT} (direkt erreichbar)`);
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Failed to sync database:', err);
|
||||
process.exit(1);
|
||||
|
||||
507
backend/services/calendarService.js
Normal file
507
backend/services/calendarService.js
Normal file
@@ -0,0 +1,507 @@
|
||||
import CalendarEvent from '../models/community/calendar_event.js';
|
||||
import User from '../models/community/user.js';
|
||||
import Friendship from '../models/community/friendship.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
import UserParamType from '../models/type/user_param.js';
|
||||
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
||||
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
class CalendarService {
|
||||
/**
|
||||
* Get all calendar events for a user
|
||||
* @param {string} hashedUserId - The user's hashed ID
|
||||
* @param {object} options - Optional filters (startDate, endDate)
|
||||
*/
|
||||
async getEvents(hashedUserId, options = {}) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const where = { userId: user.id };
|
||||
|
||||
// Filter by date range if provided
|
||||
if (options.startDate || options.endDate) {
|
||||
where[Op.or] = [];
|
||||
|
||||
if (options.startDate && options.endDate) {
|
||||
// Events that overlap with the requested range
|
||||
where[Op.or].push({
|
||||
startDate: { [Op.between]: [options.startDate, options.endDate] }
|
||||
});
|
||||
where[Op.or].push({
|
||||
endDate: { [Op.between]: [options.startDate, options.endDate] }
|
||||
});
|
||||
where[Op.or].push({
|
||||
[Op.and]: [
|
||||
{ startDate: { [Op.lte]: options.startDate } },
|
||||
{ endDate: { [Op.gte]: options.endDate } }
|
||||
]
|
||||
});
|
||||
} else if (options.startDate) {
|
||||
where[Op.or].push({ startDate: { [Op.gte]: options.startDate } });
|
||||
where[Op.or].push({ endDate: { [Op.gte]: options.startDate } });
|
||||
} else if (options.endDate) {
|
||||
where[Op.or].push({ startDate: { [Op.lte]: options.endDate } });
|
||||
}
|
||||
}
|
||||
|
||||
const events = await CalendarEvent.findAll({
|
||||
where,
|
||||
order: [['startDate', 'ASC'], ['startTime', 'ASC']]
|
||||
});
|
||||
|
||||
return events.map(e => this.formatEvent(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single event by ID
|
||||
*/
|
||||
async getEvent(hashedUserId, eventId) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.findOne({
|
||||
where: { id: eventId, userId: user.id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
return this.formatEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new calendar event
|
||||
*/
|
||||
async createEvent(hashedUserId, eventData) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.create({
|
||||
userId: user.id,
|
||||
title: eventData.title,
|
||||
description: eventData.description || null,
|
||||
categoryId: eventData.categoryId || 'personal',
|
||||
startDate: eventData.startDate,
|
||||
endDate: eventData.endDate || eventData.startDate,
|
||||
startTime: eventData.allDay ? null : eventData.startTime,
|
||||
endTime: eventData.allDay ? null : eventData.endTime,
|
||||
allDay: eventData.allDay || false
|
||||
});
|
||||
|
||||
return this.formatEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing calendar event
|
||||
*/
|
||||
async updateEvent(hashedUserId, eventId, eventData) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.findOne({
|
||||
where: { id: eventId, userId: user.id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
await event.update({
|
||||
title: eventData.title,
|
||||
description: eventData.description || null,
|
||||
categoryId: eventData.categoryId || 'personal',
|
||||
startDate: eventData.startDate,
|
||||
endDate: eventData.endDate || eventData.startDate,
|
||||
startTime: eventData.allDay ? null : eventData.startTime,
|
||||
endTime: eventData.allDay ? null : eventData.endTime,
|
||||
allDay: eventData.allDay || false
|
||||
});
|
||||
|
||||
return this.formatEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calendar event
|
||||
*/
|
||||
async deleteEvent(hashedUserId, eventId) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.findOne({
|
||||
where: { id: eventId, userId: user.id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
await event.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get friends' birthdays that are visible to the user
|
||||
* @param {string} hashedUserId - The user's hashed ID
|
||||
* @param {number} year - The year to get birthdays for
|
||||
*/
|
||||
async getFriendsBirthdays(hashedUserId, year) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
// Get user's age for visibility check
|
||||
const userAge = await this.getUserAge(user.id);
|
||||
|
||||
// Get all accepted friendships
|
||||
const friendships = await Friendship.findAll({
|
||||
where: {
|
||||
accepted: true,
|
||||
withdrawn: false,
|
||||
denied: false,
|
||||
[Op.or]: [
|
||||
{ user1Id: user.id },
|
||||
{ user2Id: user.id }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const birthdays = [];
|
||||
|
||||
for (const friendship of friendships) {
|
||||
// Get the friend's user ID
|
||||
const friendId = friendship.user1Id === user.id ? friendship.user2Id : friendship.user1Id;
|
||||
|
||||
// Get the friend's birthdate param with visibility
|
||||
const birthdateParam = await UserParam.findOne({
|
||||
where: { userId: friendId },
|
||||
include: [
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: 'birthdate' }
|
||||
},
|
||||
{
|
||||
model: UserParamVisibility,
|
||||
as: 'param_visibilities',
|
||||
include: [{
|
||||
model: UserParamVisibilityType,
|
||||
as: 'visibility_type'
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!birthdateParam || !birthdateParam.value) continue;
|
||||
|
||||
// Check visibility
|
||||
const visibility = birthdateParam.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
|
||||
if (!this.isBirthdayVisibleToFriend(visibility, userAge)) continue;
|
||||
|
||||
// Get friend's username
|
||||
const friend = await User.findOne({
|
||||
where: { id: friendId },
|
||||
attributes: ['username', 'hashedId']
|
||||
});
|
||||
|
||||
if (!friend) continue;
|
||||
|
||||
// Parse birthdate and create birthday event for the requested year
|
||||
const birthdate = new Date(birthdateParam.value);
|
||||
if (isNaN(birthdate.getTime())) continue;
|
||||
|
||||
const birthdayDate = `${year}-${String(birthdate.getMonth() + 1).padStart(2, '0')}-${String(birthdate.getDate()).padStart(2, '0')}`;
|
||||
|
||||
birthdays.push({
|
||||
id: `birthday-${friend.hashedId}-${year}`,
|
||||
title: friend.username,
|
||||
categoryId: 'birthday',
|
||||
startDate: birthdayDate,
|
||||
endDate: birthdayDate,
|
||||
allDay: true,
|
||||
isBirthday: true,
|
||||
friendHashedId: friend.hashedId
|
||||
});
|
||||
}
|
||||
|
||||
return birthdays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if birthdate is visible to a friend
|
||||
*/
|
||||
isBirthdayVisibleToFriend(visibility, requestingUserAge) {
|
||||
// Visible to friends if visibility is 'All', 'Friends', or 'FriendsAndAdults' (if adult)
|
||||
return visibility === 'All' ||
|
||||
visibility === 'Friends' ||
|
||||
(visibility === 'FriendsAndAdults' && requestingUserAge >= 18);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's age from birthdate
|
||||
*/
|
||||
async getUserAge(userId) {
|
||||
const birthdateParam = await UserParam.findOne({
|
||||
where: { userId },
|
||||
include: [{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: 'birthdate' }
|
||||
}]
|
||||
});
|
||||
|
||||
if (!birthdateParam || !birthdateParam.value) return 0;
|
||||
|
||||
const birthdate = new Date(birthdateParam.value);
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birthdate.getFullYear();
|
||||
const monthDiff = today.getMonth() - birthdate.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthdate.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upcoming birthdays for widget (sorted by next occurrence)
|
||||
*/
|
||||
async getUpcomingBirthdays(hashedUserId, limit = 10) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const userAge = await this.getUserAge(user.id);
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
|
||||
// Get all accepted friendships
|
||||
const friendships = await Friendship.findAll({
|
||||
where: {
|
||||
accepted: true,
|
||||
withdrawn: false,
|
||||
denied: false,
|
||||
[Op.or]: [
|
||||
{ user1Id: user.id },
|
||||
{ user2Id: user.id }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const birthdays = [];
|
||||
|
||||
for (const friendship of friendships) {
|
||||
const friendId = friendship.user1Id === user.id ? friendship.user2Id : friendship.user1Id;
|
||||
|
||||
const birthdateParam = await UserParam.findOne({
|
||||
where: { userId: friendId },
|
||||
include: [
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: 'birthdate' }
|
||||
},
|
||||
{
|
||||
model: UserParamVisibility,
|
||||
as: 'param_visibilities',
|
||||
include: [{
|
||||
model: UserParamVisibilityType,
|
||||
as: 'visibility_type'
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!birthdateParam || !birthdateParam.value) continue;
|
||||
|
||||
const visibility = birthdateParam.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
|
||||
if (!this.isBirthdayVisibleToFriend(visibility, userAge)) continue;
|
||||
|
||||
const friend = await User.findOne({
|
||||
where: { id: friendId },
|
||||
attributes: ['username', 'hashedId']
|
||||
});
|
||||
|
||||
if (!friend) continue;
|
||||
|
||||
const birthdate = new Date(birthdateParam.value);
|
||||
if (isNaN(birthdate.getTime())) continue;
|
||||
|
||||
// Calculate next birthday
|
||||
let nextBirthday = new Date(currentYear, birthdate.getMonth(), birthdate.getDate());
|
||||
if (nextBirthday < today) {
|
||||
nextBirthday = new Date(currentYear + 1, birthdate.getMonth(), birthdate.getDate());
|
||||
}
|
||||
|
||||
// Calculate days until birthday
|
||||
const diffTime = nextBirthday.getTime() - today.getTime();
|
||||
const daysUntil = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Calculate age they will turn
|
||||
const turningAge = nextBirthday.getFullYear() - birthdate.getFullYear();
|
||||
|
||||
birthdays.push({
|
||||
username: friend.username,
|
||||
hashedId: friend.hashedId,
|
||||
date: `${String(birthdate.getMonth() + 1).padStart(2, '0')}-${String(birthdate.getDate()).padStart(2, '0')}`,
|
||||
nextDate: nextBirthday.toISOString().split('T')[0],
|
||||
daysUntil,
|
||||
turningAge
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by days until birthday
|
||||
birthdays.sort((a, b) => a.daysUntil - b.daysUntil);
|
||||
|
||||
return birthdays.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upcoming events for widget
|
||||
*/
|
||||
async getUpcomingEvents(hashedUserId, limit = 10) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
|
||||
const events = await CalendarEvent.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
[Op.or]: [
|
||||
{ startDate: { [Op.gte]: todayStr } },
|
||||
{ endDate: { [Op.gte]: todayStr } }
|
||||
]
|
||||
},
|
||||
order: [['startDate', 'ASC'], ['startTime', 'ASC']],
|
||||
limit
|
||||
});
|
||||
|
||||
return events.map(e => ({
|
||||
id: e.id,
|
||||
titel: e.title,
|
||||
datum: e.startDate,
|
||||
beschreibung: e.description || null,
|
||||
categoryId: e.categoryId,
|
||||
allDay: e.allDay,
|
||||
startTime: e.startTime ? e.startTime.substring(0, 5) : null,
|
||||
endDate: e.endDate
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mini calendar data for widget
|
||||
*/
|
||||
async getMiniCalendarData(hashedUserId) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth();
|
||||
|
||||
// Get first and last day of month
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
|
||||
const startStr = firstDay.toISOString().split('T')[0];
|
||||
const endStr = lastDay.toISOString().split('T')[0];
|
||||
|
||||
// Get user events for this month
|
||||
const events = await CalendarEvent.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
[Op.or]: [
|
||||
{ startDate: { [Op.between]: [startStr, endStr] } },
|
||||
{ endDate: { [Op.between]: [startStr, endStr] } },
|
||||
{
|
||||
[Op.and]: [
|
||||
{ startDate: { [Op.lte]: startStr } },
|
||||
{ endDate: { [Op.gte]: endStr } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Get birthdays for this month
|
||||
const birthdays = await this.getFriendsBirthdays(hashedUserId, year);
|
||||
const monthBirthdays = birthdays.filter(b => {
|
||||
const bMonth = parseInt(b.startDate.split('-')[1]);
|
||||
return bMonth === month + 1;
|
||||
});
|
||||
|
||||
// Build days with events
|
||||
const daysWithEvents = {};
|
||||
|
||||
for (const event of events) {
|
||||
const start = new Date(event.startDate);
|
||||
const end = event.endDate ? new Date(event.endDate) : start;
|
||||
|
||||
for (let d = new Date(start); d <= end && d <= lastDay; d.setDate(d.getDate() + 1)) {
|
||||
if (d >= firstDay) {
|
||||
const dayNum = d.getDate();
|
||||
if (!daysWithEvents[dayNum]) {
|
||||
daysWithEvents[dayNum] = { events: 0, birthdays: 0 };
|
||||
}
|
||||
daysWithEvents[dayNum].events++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const birthday of monthBirthdays) {
|
||||
const dayNum = parseInt(birthday.startDate.split('-')[2]);
|
||||
if (!daysWithEvents[dayNum]) {
|
||||
daysWithEvents[dayNum] = { events: 0, birthdays: 0 };
|
||||
}
|
||||
daysWithEvents[dayNum].birthdays++;
|
||||
}
|
||||
|
||||
return {
|
||||
year,
|
||||
month: month + 1,
|
||||
today: today.getDate(),
|
||||
firstDayOfWeek: firstDay.getDay() === 0 ? 7 : firstDay.getDay(), // Monday = 1
|
||||
daysInMonth: lastDay.getDate(),
|
||||
daysWithEvents
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format event for API response
|
||||
*/
|
||||
formatEvent(event) {
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
categoryId: event.categoryId,
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate,
|
||||
startTime: event.startTime ? event.startTime.substring(0, 5) : null, // HH:MM format
|
||||
endTime: event.endTime ? event.endTime.substring(0, 5) : null,
|
||||
allDay: event.allDay,
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: event.updatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new CalendarService();
|
||||
59
backend/services/dashboardService.js
Normal file
59
backend/services/dashboardService.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import BaseService from './BaseService.js';
|
||||
import UserDashboard from '../models/community/user_dashboard.js';
|
||||
import WidgetType from '../models/type/widget_type.js';
|
||||
|
||||
class DashboardService extends BaseService {
|
||||
/**
|
||||
* Liste aller möglichen (verfügbaren) Widget-Typen.
|
||||
* @returns {Promise<Array<{ id: number, label: string, endpoint: string, description: string|null, orderId: number }>>}
|
||||
*/
|
||||
async getAvailableWidgets() {
|
||||
const rows = await WidgetType.findAll({
|
||||
order: [['orderId', 'ASC'], ['id', 'ASC']],
|
||||
attributes: ['id', 'label', 'endpoint', 'description', 'orderId']
|
||||
});
|
||||
return rows.map(r => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
endpoint: r.endpoint,
|
||||
description: r.description ?? null,
|
||||
orderId: r.orderId
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hashedUserId
|
||||
* @returns {Promise<{ widgets: Array<{ id: string, title: string, endpoint: string }> }>}
|
||||
*/
|
||||
async getConfig(hashedUserId) {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
const row = await UserDashboard.findOne({ where: { userId: user.id } });
|
||||
const config = row?.config ?? { widgets: [] };
|
||||
if (!Array.isArray(config.widgets)) config.widgets = [];
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hashedUserId
|
||||
* @param {{ widgets: Array<{ id: string, title: string, endpoint: string }> }} config
|
||||
*/
|
||||
async setConfig(hashedUserId, config) {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
const widgets = Array.isArray(config?.widgets) ? config.widgets : [];
|
||||
const sanitized = widgets.map(w => ({
|
||||
id: String(w?.id ?? ''),
|
||||
title: String(w?.title ?? ''),
|
||||
endpoint: String(w?.endpoint ?? '')
|
||||
})).filter(w => w.id && (w.title || w.endpoint));
|
||||
const payload = { widgets: sanitized };
|
||||
const existing = await UserDashboard.findOne({ where: { userId: user.id } });
|
||||
if (existing) {
|
||||
await existing.update({ config: payload });
|
||||
} else {
|
||||
await UserDashboard.create({ userId: user.id, config: payload });
|
||||
}
|
||||
return { widgets: sanitized };
|
||||
}
|
||||
}
|
||||
|
||||
export default new DashboardService();
|
||||
File diff suppressed because it is too large
Load Diff
125
backend/services/modelsProxyService.js
Normal file
125
backend/services/modelsProxyService.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Model-Proxy-Service: Lädt GLB-Dateien, komprimiert sie mit gltf-transform (Draco + Textur-Optimierung)
|
||||
* und legt sie im Datei-Cache ab. Weitere Requests werden aus dem Cache bedient.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const BACKEND_DIR = path.join(__dirname, '..');
|
||||
const PROJECT_ROOT = path.join(BACKEND_DIR, '..');
|
||||
const MODELS_REL = path.join('models', '3d', 'falukant', 'characters');
|
||||
const DIST_MODELS = path.join(PROJECT_ROOT, 'frontend', 'dist', MODELS_REL);
|
||||
const PUBLIC_MODELS = path.join(PROJECT_ROOT, 'frontend', 'public', MODELS_REL);
|
||||
const CACHE_DIR = path.join(BACKEND_DIR, 'data', 'model-cache');
|
||||
const CLI_PATH = path.join(BACKEND_DIR, 'node_modules', '.bin', 'gltf-transform');
|
||||
|
||||
/** Einmal ermitteltes Quellverzeichnis (frontend/dist oder frontend/public). */
|
||||
let _sourceDir = null;
|
||||
|
||||
/** Production: frontend/dist; Local: frontend/public. Einmal pro Prozess festgelegt, damit
|
||||
* isCacheValid() stets gegen dieselbe Quelle prüft (kein Wechsel zwischen dist/public). */
|
||||
function getSourceDir() {
|
||||
if (_sourceDir !== null) return _sourceDir;
|
||||
_sourceDir = fs.existsSync(DIST_MODELS) ? DIST_MODELS : PUBLIC_MODELS;
|
||||
return _sourceDir;
|
||||
}
|
||||
|
||||
/** Erlaubte Dateinamen (nur [a-z0-9_.-]+.glb) */
|
||||
const FILENAME_RE = /^[a-z0-9_.-]+\.glb$/i;
|
||||
|
||||
/** Laufende Optimierungen pro Dateiname → Promise<string> (Cache-Pfad) */
|
||||
const pending = new Map();
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass der Cache-Ordner existiert.
|
||||
*/
|
||||
function ensureCacheDir() {
|
||||
if (!fs.existsSync(CACHE_DIR)) {
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob die Cache-Datei gültig ist (existiert und ist nicht älter als die Quelle).
|
||||
* @param {string} sourcePath
|
||||
* @param {string} cachePath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isCacheValid(sourcePath, cachePath) {
|
||||
if (!fs.existsSync(cachePath)) return false;
|
||||
if (!fs.existsSync(sourcePath)) return false;
|
||||
const sourceStat = fs.statSync(sourcePath);
|
||||
const cacheStat = fs.statSync(cachePath);
|
||||
return cacheStat.mtimeMs >= sourceStat.mtimeMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt gltf-transform optimize aus (Draco + texture-size 1024).
|
||||
* @param {string} inputPath
|
||||
* @param {string} outputPath
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function runOptimize(inputPath, outputPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(
|
||||
'node',
|
||||
[CLI_PATH, 'optimize', inputPath, outputPath, '--compress', 'draco', '--texture-size', '1024'],
|
||||
{ cwd: BACKEND_DIR, stdio: ['ignore', 'pipe', 'pipe'] }
|
||||
);
|
||||
let stderr = '';
|
||||
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`gltf-transform exit ${code}: ${stderr}`));
|
||||
});
|
||||
child.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Pfad zur optimierten (gecachten) GLB-Datei.
|
||||
* Erstellt die optimierte Datei per gltf-transform, falls nicht (gültig) gecacht.
|
||||
*
|
||||
* @param {string} filename - z.B. "male_child.glb"
|
||||
* @returns {Promise<string>} Absoluter Pfad zur optimierten Datei (Cache)
|
||||
* @throws {Error} Bei ungültigem Dateinamen oder fehlender Quelldatei
|
||||
*/
|
||||
export async function getOptimizedModelPath(filename) {
|
||||
if (!FILENAME_RE.test(filename)) {
|
||||
throw new Error(`Invalid model filename: ${filename}`);
|
||||
}
|
||||
|
||||
const sourceDir = getSourceDir();
|
||||
const sourcePath = path.join(sourceDir, filename);
|
||||
const cacheFilename = filename.replace(/\.glb$/, '_opt.glb');
|
||||
const cachePath = path.join(CACHE_DIR, cacheFilename);
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
throw new Error(`Source model not found: ${filename} (looked in ${sourceDir})`);
|
||||
}
|
||||
|
||||
ensureCacheDir();
|
||||
|
||||
if (isCacheValid(sourcePath, cachePath)) {
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
let promise = pending.get(filename);
|
||||
if (!promise) {
|
||||
promise = (async () => {
|
||||
try {
|
||||
await runOptimize(sourcePath, cachePath);
|
||||
return cachePath;
|
||||
} finally {
|
||||
pending.delete(filename);
|
||||
}
|
||||
})();
|
||||
pending.set(filename, promise);
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
123
backend/services/newsService.js
Normal file
123
backend/services/newsService.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Proxy für newsdata.io API.
|
||||
* Endpoint: https://newsdata.io/api/1/news?apikey=...&language=...&category=...
|
||||
* Pagination: counter = wievieltes Widget dieser Art (0 = erste Seite, 1 = zweite, …), damit News nicht doppelt gezeigt werden.
|
||||
*/
|
||||
|
||||
const NEWS_BASE = 'https://newsdata.io/api/1/news';
|
||||
|
||||
// Cache für News-Ergebnisse (pro Sprache/Kategorie)
|
||||
const newsCache = new Map();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten Cache
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {number} options.counter - 0 = erste Seite, 1 = zweite, … (für Pagination/nextPage)
|
||||
* @param {string} [options.language] - z. B. de, en
|
||||
* @param {string} [options.category] - z. B. top, technology
|
||||
* @returns {Promise<{ results: Array, nextPage: string|null }>}
|
||||
*/
|
||||
async function fetchNewsPage({ language = 'de', category = 'top', nextPageToken = null }) {
|
||||
const apiKey = process.env.NEWSDATA_IO_API_KEY;
|
||||
if (!apiKey || !apiKey.trim()) {
|
||||
throw new Error('NEWSDATA_IO_API_KEY is not set in .env');
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set('apikey', apiKey.trim());
|
||||
params.set('language', String(language));
|
||||
params.set('category', String(category));
|
||||
if (nextPageToken) params.set('page', nextPageToken);
|
||||
|
||||
const url = `${NEWS_BASE}?${params.toString()}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`newsdata.io: ${res.status} ${text.slice(0, 200)}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return {
|
||||
results: data.results ?? [],
|
||||
nextPage: data.nextPage ?? null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt gecachte Artikel oder lädt sie von der API
|
||||
*/
|
||||
async function getCachedNews({ language = 'de', category = 'top', minArticles = 10 }) {
|
||||
const cacheKey = `${language}:${category}`;
|
||||
const cached = newsCache.get(cacheKey);
|
||||
|
||||
// Cache gültig?
|
||||
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
|
||||
// Wenn wir mehr Artikel brauchen, erweitern
|
||||
if (cached.articles.length >= minArticles) {
|
||||
return cached.articles;
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Daten laden
|
||||
const collected = cached?.articles || [];
|
||||
let nextPageToken = cached?.nextPage || null;
|
||||
|
||||
while (collected.length < minArticles) {
|
||||
try {
|
||||
const page = await fetchNewsPage({
|
||||
language,
|
||||
category,
|
||||
nextPageToken: nextPageToken || undefined
|
||||
});
|
||||
const items = page.results ?? [];
|
||||
|
||||
// Duplikate vermeiden (nach title)
|
||||
const existingTitles = new Set(collected.map(a => a.title));
|
||||
for (const item of items) {
|
||||
if (!existingTitles.has(item.title)) {
|
||||
collected.push(item);
|
||||
existingTitles.add(item.title);
|
||||
}
|
||||
}
|
||||
|
||||
nextPageToken = page.nextPage ?? null;
|
||||
if (items.length === 0 || !nextPageToken) break;
|
||||
} catch (error) {
|
||||
console.error('News fetch error:', error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache aktualisieren
|
||||
newsCache.set(cacheKey, {
|
||||
articles: collected,
|
||||
nextPage: nextPageToken,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den N-ten Artikel (counter = 0, 1, 2, …) für das N-te News-Widget.
|
||||
* Nutzt Cache, damit mehrere Widgets unterschiedliche Artikel bekommen.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {number} options.counter - Index des Artikels (0 = erster, 1 = zweiter, …)
|
||||
* @param {string} [options.language]
|
||||
* @param {string} [options.category]
|
||||
* @returns {Promise<{ results: Array, nextPage: string|null }>}
|
||||
*/
|
||||
async function getNews({ counter = 0, language = 'de', category = 'top' }) {
|
||||
const neededIndex = Math.max(0, counter);
|
||||
|
||||
// Mindestens so viele Artikel laden wie benötigt
|
||||
const articles = await getCachedNews({
|
||||
language,
|
||||
category,
|
||||
minArticles: neededIndex + 1
|
||||
});
|
||||
|
||||
const single = articles[neededIndex] ? [articles[neededIndex]] : [];
|
||||
return { results: single, nextPage: null };
|
||||
}
|
||||
|
||||
export default { getNews };
|
||||
File diff suppressed because it is too large
Load Diff
24
backend/sql/add-native-language-to-courses.sql
Normal file
24
backend/sql/add-native-language-to-courses.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- ============================================
|
||||
-- Füge native_language_id zu vocab_course hinzu
|
||||
-- ============================================
|
||||
-- Dieses Feld speichert die Muttersprache des Lerners
|
||||
-- z.B. "Bisaya für Deutschsprachige" -> language_id = Bisaya, native_language_id = Deutsch
|
||||
|
||||
-- Spalte hinzufügen
|
||||
ALTER TABLE community.vocab_course
|
||||
ADD COLUMN IF NOT EXISTS native_language_id INTEGER;
|
||||
|
||||
-- Foreign Key Constraint hinzufügen
|
||||
ALTER TABLE community.vocab_course
|
||||
ADD CONSTRAINT vocab_course_native_language_fk
|
||||
FOREIGN KEY (native_language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Index für bessere Performance
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx
|
||||
ON community.vocab_course(native_language_id);
|
||||
|
||||
-- Kommentar hinzufügen
|
||||
COMMENT ON COLUMN community.vocab_course.native_language_id IS
|
||||
'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".';
|
||||
16
backend/sql/add-speaking-exercise-types.sql
Normal file
16
backend/sql/add-speaking-exercise-types.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- ============================================
|
||||
-- Neue Übungstypen für Sprachproduktion hinzufügen
|
||||
-- ============================================
|
||||
-- Führe diese Queries direkt auf dem Server aus
|
||||
|
||||
-- Neue Übungstypen hinzufügen
|
||||
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
||||
('reading_aloud', 'Laut vorlesen - Übung zur Verbesserung der Aussprache'),
|
||||
('speaking_from_memory', 'Aus dem Kopf sprechen - Übung zur aktiven Sprachproduktion')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- Hinweis:
|
||||
-- - reading_aloud: Text wird angezeigt, User liest vor, Speech Recognition prüft
|
||||
-- - speaking_from_memory: Prompt wird angezeigt, User spricht frei, manuelle/automatische Bewertung
|
||||
-- ============================================
|
||||
242
backend/sql/create-vocab-courses.sql
Normal file
242
backend/sql/create-vocab-courses.sql
Normal file
@@ -0,0 +1,242 @@
|
||||
-- ============================================
|
||||
-- Vocab Courses - Vollständige SQL-Installation
|
||||
-- ============================================
|
||||
-- Führe diese Queries direkt auf dem Server aus
|
||||
-- Reihenfolge beachten!
|
||||
|
||||
-- ============================================
|
||||
-- 1. Kurs-Tabellen erstellen
|
||||
-- ============================================
|
||||
|
||||
-- Kurs-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course (
|
||||
id SERIAL PRIMARY KEY,
|
||||
owner_user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
language_id INTEGER NOT NULL,
|
||||
difficulty_level INTEGER DEFAULT 1,
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
share_code TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_owner_fk
|
||||
FOREIGN KEY (owner_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_language_fk
|
||||
FOREIGN KEY (language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
|
||||
);
|
||||
|
||||
-- Lektionen innerhalb eines Kurses
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
|
||||
id SERIAL PRIMARY KEY,
|
||||
course_id INTEGER NOT NULL,
|
||||
chapter_id INTEGER,
|
||||
lesson_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
week_number INTEGER,
|
||||
day_number INTEGER,
|
||||
lesson_type TEXT DEFAULT 'vocab',
|
||||
audio_url TEXT,
|
||||
cultural_notes TEXT,
|
||||
target_minutes INTEGER,
|
||||
target_score_percent INTEGER DEFAULT 80,
|
||||
requires_review BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_lesson_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_lesson_chapter_fk
|
||||
FOREIGN KEY (chapter_id)
|
||||
REFERENCES community.vocab_chapter(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
|
||||
);
|
||||
|
||||
-- Einschreibungen in Kurse
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
course_id INTEGER NOT NULL,
|
||||
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_enrollment_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_enrollment_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
|
||||
);
|
||||
|
||||
-- Fortschritt pro User und Lektion
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
course_id INTEGER NOT NULL,
|
||||
lesson_id INTEGER NOT NULL,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
score INTEGER DEFAULT 0,
|
||||
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT vocab_course_progress_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_lesson_fk
|
||||
FOREIGN KEY (lesson_id)
|
||||
REFERENCES community.vocab_course_lesson(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 2. Grammatik-Übungstabellen erstellen
|
||||
-- ============================================
|
||||
|
||||
-- Grammatik-Übungstypen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Grammatik-Übungen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
|
||||
id SERIAL PRIMARY KEY,
|
||||
lesson_id INTEGER NOT NULL,
|
||||
exercise_type_id INTEGER NOT NULL,
|
||||
exercise_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
instruction TEXT,
|
||||
question_data JSONB NOT NULL,
|
||||
answer_data JSONB NOT NULL,
|
||||
explanation TEXT,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_grammar_exercise_lesson_fk
|
||||
FOREIGN KEY (lesson_id)
|
||||
REFERENCES community.vocab_course_lesson(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_type_fk
|
||||
FOREIGN KEY (exercise_type_id)
|
||||
REFERENCES community.vocab_grammar_exercise_type(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_creator_fk
|
||||
FOREIGN KEY (created_by_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
|
||||
);
|
||||
|
||||
-- Fortschritt für Grammatik-Übungen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
exercise_id INTEGER NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
correct_attempts INTEGER DEFAULT 0,
|
||||
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
|
||||
FOREIGN KEY (exercise_id)
|
||||
REFERENCES community.vocab_grammar_exercise(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 3. Indizes erstellen
|
||||
-- ============================================
|
||||
|
||||
-- Kurs-Indizes
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
|
||||
ON community.vocab_course(owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
|
||||
ON community.vocab_course(language_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
|
||||
ON community.vocab_course(is_public);
|
||||
|
||||
-- Lektion-Indizes
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
|
||||
ON community.vocab_course_lesson(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
|
||||
ON community.vocab_course_lesson(chapter_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
|
||||
ON community.vocab_course_lesson(course_id, week_number);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
|
||||
ON community.vocab_course_lesson(lesson_type);
|
||||
|
||||
-- Einschreibungs-Indizes
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
|
||||
ON community.vocab_course_enrollment(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
|
||||
ON community.vocab_course_enrollment(course_id);
|
||||
|
||||
-- Fortschritts-Indizes
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
|
||||
ON community.vocab_course_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
|
||||
ON community.vocab_course_progress(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
|
||||
ON community.vocab_course_progress(lesson_id);
|
||||
|
||||
-- Grammatik-Übungs-Indizes
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
|
||||
ON community.vocab_grammar_exercise(lesson_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
|
||||
ON community.vocab_grammar_exercise(exercise_type_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
|
||||
ON community.vocab_grammar_exercise_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
|
||||
ON community.vocab_grammar_exercise_progress(exercise_id);
|
||||
|
||||
-- ============================================
|
||||
-- 4. Standard-Daten einfügen
|
||||
-- ============================================
|
||||
|
||||
-- Standard-Übungstypen für Grammatik
|
||||
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
||||
('gap_fill', 'Lückentext-Übung'),
|
||||
('multiple_choice', 'Multiple-Choice-Fragen'),
|
||||
('sentence_building', 'Satzbau-Übung'),
|
||||
('transformation', 'Satzumformung'),
|
||||
('conjugation', 'Konjugations-Übung'),
|
||||
('declension', 'Deklinations-Übung')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 5. Kommentare hinzufügen (optional)
|
||||
-- ============================================
|
||||
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
|
||||
'Type: vocab, grammar, conversation, culture, review';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
|
||||
'Zielzeit in Minuten für diese Lektion';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
|
||||
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
|
||||
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
|
||||
|
||||
-- ============================================
|
||||
-- Fertig!
|
||||
-- ============================================
|
||||
-- Alle Tabellen, Indizes und Standard-Daten wurden erstellt.
|
||||
-- Du kannst jetzt Kurse erstellen und verwenden.
|
||||
131
backend/sql/update-vocab-courses-existing.sql
Normal file
131
backend/sql/update-vocab-courses-existing.sql
Normal file
@@ -0,0 +1,131 @@
|
||||
-- ============================================
|
||||
-- Vocab Courses - Update für bestehende Installation
|
||||
-- ============================================
|
||||
-- Führe diese Queries aus, wenn die Tabellen bereits existieren
|
||||
-- (z.B. wenn nur die Basis-Tabellen erstellt wurden)
|
||||
|
||||
-- ============================================
|
||||
-- 1. chapter_id optional machen
|
||||
-- ============================================
|
||||
ALTER TABLE community.vocab_course_lesson
|
||||
ALTER COLUMN chapter_id DROP NOT NULL;
|
||||
|
||||
-- ============================================
|
||||
-- 2. Neue Spalten zu vocab_course_lesson hinzufügen
|
||||
-- ============================================
|
||||
ALTER TABLE community.vocab_course_lesson
|
||||
ADD COLUMN IF NOT EXISTS week_number INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS day_number INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS lesson_type TEXT DEFAULT 'vocab',
|
||||
ADD COLUMN IF NOT EXISTS audio_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS cultural_notes TEXT,
|
||||
ADD COLUMN IF NOT EXISTS target_minutes INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS target_score_percent INTEGER DEFAULT 80,
|
||||
ADD COLUMN IF NOT EXISTS requires_review BOOLEAN DEFAULT false;
|
||||
|
||||
-- ============================================
|
||||
-- 3. Neue Indizes hinzufügen
|
||||
-- ============================================
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
|
||||
ON community.vocab_course_lesson(course_id, week_number);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
|
||||
ON community.vocab_course_lesson(lesson_type);
|
||||
|
||||
-- ============================================
|
||||
-- 4. Grammatik-Übungstabellen erstellen (falls noch nicht vorhanden)
|
||||
-- ============================================
|
||||
|
||||
-- Grammatik-Übungstypen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Grammatik-Übungen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
|
||||
id SERIAL PRIMARY KEY,
|
||||
lesson_id INTEGER NOT NULL,
|
||||
exercise_type_id INTEGER NOT NULL,
|
||||
exercise_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
instruction TEXT,
|
||||
question_data JSONB NOT NULL,
|
||||
answer_data JSONB NOT NULL,
|
||||
explanation TEXT,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_grammar_exercise_lesson_fk
|
||||
FOREIGN KEY (lesson_id)
|
||||
REFERENCES community.vocab_course_lesson(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_type_fk
|
||||
FOREIGN KEY (exercise_type_id)
|
||||
REFERENCES community.vocab_grammar_exercise_type(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_creator_fk
|
||||
FOREIGN KEY (created_by_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
|
||||
);
|
||||
|
||||
-- Fortschritt für Grammatik-Übungen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
exercise_id INTEGER NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
correct_attempts INTEGER DEFAULT 0,
|
||||
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
|
||||
FOREIGN KEY (exercise_id)
|
||||
REFERENCES community.vocab_grammar_exercise(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
|
||||
);
|
||||
|
||||
-- Indizes für Grammatik-Übungen
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
|
||||
ON community.vocab_grammar_exercise(lesson_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
|
||||
ON community.vocab_grammar_exercise(exercise_type_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
|
||||
ON community.vocab_grammar_exercise_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
|
||||
ON community.vocab_grammar_exercise_progress(exercise_id);
|
||||
|
||||
-- ============================================
|
||||
-- 5. Standard-Daten einfügen
|
||||
-- ============================================
|
||||
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
||||
('gap_fill', 'Lückentext-Übung'),
|
||||
('multiple_choice', 'Multiple-Choice-Fragen'),
|
||||
('sentence_building', 'Satzbau-Übung'),
|
||||
('transformation', 'Satzumformung'),
|
||||
('conjugation', 'Konjugations-Übung'),
|
||||
('declension', 'Deklinations-Übung')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- 6. Kommentare hinzufügen
|
||||
-- ============================================
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.lesson_type IS
|
||||
'Type: vocab, grammar, conversation, culture, review';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.target_minutes IS
|
||||
'Zielzeit in Minuten für diese Lektion';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.target_score_percent IS
|
||||
'Mindestpunktzahl in Prozent zum Abschluss (z.B. 80)';
|
||||
COMMENT ON COLUMN community.vocab_course_lesson.requires_review IS
|
||||
'Muss diese Lektion wiederholt werden, wenn Ziel nicht erreicht?';
|
||||
|
||||
-- ============================================
|
||||
-- Fertig!
|
||||
-- ============================================
|
||||
43
backend/sync-tables-only.js
Normal file
43
backend/sync-tables-only.js
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Einfaches Script zum Erstellen/Aktualisieren von Tabellen
|
||||
* Ohne Cleanup und Initialisierung
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { initializeDatabase, syncModelsAlways, sequelize } from './utils/sequelize.js';
|
||||
import setupAssociations from './models/associations.js';
|
||||
import models from './models/index.js';
|
||||
|
||||
console.log('🗄️ Starte Tabellen-Synchronisation (nur Schema-Updates)...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 1. Datenbank-Schemas initialisieren
|
||||
console.log('📊 Initialisiere Datenbank-Schemas...');
|
||||
await initializeDatabase();
|
||||
console.log('✅ Datenbank-Schemas initialisiert');
|
||||
|
||||
// 2. Associations setzen
|
||||
console.log('🔗 Setze Associations...');
|
||||
setupAssociations();
|
||||
console.log('✅ Associations gesetzt');
|
||||
|
||||
// 3. Nur Tabellen synchronisieren (ohne Cleanup, ohne Initialisierung)
|
||||
console.log('🔄 Synchronisiere Tabellen...');
|
||||
await syncModelsAlways(models);
|
||||
console.log('✅ Tabellen-Synchronisation erfolgreich abgeschlossen');
|
||||
|
||||
console.log('🎉 Tabellen-Synchronisation abgeschlossen!');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei der Tabellen-Synchronisation:', error);
|
||||
console.error('Stack Trace:', error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Script ausführen
|
||||
main();
|
||||
@@ -282,7 +282,11 @@ async function initializeFalukantProducts() {
|
||||
{ labelTr: 'ox', category: 5, productionTime: 5, sellCost: 60 },
|
||||
];
|
||||
|
||||
const productsToInsert = baseProducts;
|
||||
const productsToInsert = baseProducts.map(p => ({
|
||||
...p,
|
||||
sellCostMinNeutral: Math.ceil(p.sellCost * factorMin),
|
||||
sellCostMaxNeutral: Math.ceil(p.sellCost * factorMax),
|
||||
}));
|
||||
|
||||
await ProductType.bulkCreate(productsToInsert, {
|
||||
ignoreDuplicates: true,
|
||||
|
||||
@@ -16,6 +16,8 @@ import ReputationActionType from "../../models/falukant/type/reputation_action.j
|
||||
import VehicleType from "../../models/falukant/type/vehicle.js";
|
||||
import LearnRecipient from "../../models/falukant/type/learn_recipient.js";
|
||||
import PoliticalOfficeType from "../../models/falukant/type/political_office_type.js";
|
||||
import 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 PoliticalOfficePrerequisite from "../../models/falukant/predefine/political_office_prerequisite.js";
|
||||
import UndergroundType from "../../models/falukant/type/underground.js";
|
||||
@@ -47,6 +49,8 @@ export const initializeFalukantTypes = async () => {
|
||||
await initializePoliticalOfficeBenefitTypes();
|
||||
await initializePoliticalOfficeTypes();
|
||||
await initializePoliticalOfficePrerequisites();
|
||||
await initializeChurchOfficeTypes();
|
||||
await initializeChurchOfficePrerequisites();
|
||||
await initializeUndergroundTypes();
|
||||
await initializeVehicleTypes();
|
||||
await initializeFalukantWeatherTypes();
|
||||
@@ -1024,6 +1028,136 @@ export const initializePoliticalOfficePrerequisites = async () => {
|
||||
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 () => {
|
||||
for (const underground of undergroundTypes) {
|
||||
await UndergroundType.findOrCreate({
|
||||
|
||||
30
backend/utils/initializeWidgetTypes.js
Normal file
30
backend/utils/initializeWidgetTypes.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import WidgetType from '../models/type/widget_type.js';
|
||||
|
||||
const DEFAULT_WIDGET_TYPES = [
|
||||
{ label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 },
|
||||
{ label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 },
|
||||
{ label: 'News', endpoint: '/api/news?language=de&category=top', description: 'Nachrichten (newsdata.io), Counter für Pagination', orderId: 3 },
|
||||
{ label: 'Geburtstage', endpoint: '/api/calendar/widget/birthdays', description: 'Nächste Geburtstage von Freunden', orderId: 4 },
|
||||
{ label: 'Nächste Termine', endpoint: '/api/calendar/widget/upcoming', description: 'Anstehende Kalendertermine', orderId: 5 },
|
||||
{ label: 'Kalender', endpoint: '/api/calendar/widget/mini', description: 'Mini-Kalenderansicht', orderId: 6 }
|
||||
];
|
||||
|
||||
/**
|
||||
* Stellt die Standard-Widget-Typen in type.widget_type bereit.
|
||||
* Idempotent: vorhandene Einträge werden nicht verändert.
|
||||
*/
|
||||
const initializeWidgetTypes = async () => {
|
||||
for (const row of DEFAULT_WIDGET_TYPES) {
|
||||
await WidgetType.findOrCreate({
|
||||
where: { endpoint: row.endpoint },
|
||||
defaults: {
|
||||
label: row.label,
|
||||
endpoint: row.endpoint,
|
||||
description: row.description ?? null,
|
||||
orderId: row.orderId ?? 0
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default initializeWidgetTypes;
|
||||
@@ -38,6 +38,12 @@ if (!dbName || !dbUser || !dbHost) {
|
||||
throw new Error('Missing required database environment variables: DB_NAME, DB_USER, or DB_HOST');
|
||||
}
|
||||
|
||||
const poolMax = Number.parseInt(process.env.DB_POOL_MAX || '5', 10);
|
||||
const poolMin = Number.parseInt(process.env.DB_POOL_MIN || '1', 10);
|
||||
const poolAcquire = Number.parseInt(process.env.DB_POOL_ACQUIRE || '30000', 10);
|
||||
const poolIdle = Number.parseInt(process.env.DB_POOL_IDLE || '10000', 10);
|
||||
const poolEvict = Number.parseInt(process.env.DB_POOL_EVICT || '1000', 10);
|
||||
|
||||
const sequelize = new Sequelize(dbName, dbUser, dbPass, {
|
||||
host: dbHost,
|
||||
dialect: 'postgres',
|
||||
@@ -47,8 +53,55 @@ const sequelize = new Sequelize(dbName, dbUser, dbPass, {
|
||||
},
|
||||
benchmark: SQL_BENCHMARK,
|
||||
logging: sqlLogger,
|
||||
pool: {
|
||||
max: poolMax, // Maximale Anzahl von Verbindungen im Pool
|
||||
min: poolMin, // Minimale Anzahl von Verbindungen im Pool
|
||||
acquire: poolAcquire, // Maximale Zeit (ms) zum Erwerb einer Verbindung
|
||||
idle: poolIdle, // Maximale Zeit (ms), die eine Verbindung idle sein kann, bevor sie entfernt wird
|
||||
evict: poolEvict, // Intervall (ms) zum Prüfen auf idle Verbindungen
|
||||
handleDisconnects: true // Automatisches Reconnect bei Verbindungsverlust
|
||||
},
|
||||
dialectOptions: {
|
||||
connectTimeout: 30000 // Timeout für Verbindungsaufbau (30 Sekunden)
|
||||
},
|
||||
retry: {
|
||||
max: 3, // Maximale Anzahl von Wiederholungsversuchen
|
||||
match: [
|
||||
/ConnectionError/,
|
||||
/SequelizeConnectionError/,
|
||||
/SequelizeConnectionRefusedError/,
|
||||
/SequelizeHostNotFoundError/,
|
||||
/SequelizeHostNotReachableError/,
|
||||
/SequelizeInvalidConnectionError/,
|
||||
/SequelizeConnectionTimedOutError/
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: Query mit Timeout (muss nach sequelize Initialisierung definiert werden)
|
||||
const queryWithTimeout = async (query, timeoutMs = 10000, description = 'Query', replacements = null) => {
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
const queryOptions = replacements
|
||||
? { replacements, type: sequelize.QueryTypes.SELECT }
|
||||
: {};
|
||||
|
||||
const result = await Promise.race([
|
||||
sequelize.query(query, queryOptions),
|
||||
timeoutPromise
|
||||
]);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error.message.includes('Timeout')) {
|
||||
throw error; // Re-throw für bessere Fehlerbehandlung
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const createSchemas = async () => {
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS community');
|
||||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS logs');
|
||||
@@ -435,12 +488,39 @@ async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, ch
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Sync mit Timeout
|
||||
const syncModelWithTimeout = async (model, timeoutMs = 60000) => {
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Model sync timeout after ${timeoutMs}ms`)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
model.sync({ alter: true, force: false, constraints: false }),
|
||||
timeoutPromise
|
||||
]);
|
||||
} catch (error) {
|
||||
if (error.message.includes('timeout')) {
|
||||
console.warn(` ⚠️ ${model.name} sync timeout nach ${timeoutMs}ms - überspringe...`);
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Immer Schema-Updates (für Deployment)
|
||||
const syncModelsAlways = async (models) => {
|
||||
console.log('🔍 Deployment-Modus: Führe immer Schema-Updates durch...');
|
||||
|
||||
const modelArray = Object.values(models);
|
||||
const totalModels = modelArray.length;
|
||||
let currentModel = 0;
|
||||
|
||||
try {
|
||||
for (const model of Object.values(models)) {
|
||||
for (const model of modelArray) {
|
||||
currentModel++;
|
||||
console.log(` 🔄 Syncing model ${model.name} (${currentModel}/${totalModels})...`);
|
||||
// Temporarily remove VIRTUAL fields before sync to prevent sync errors
|
||||
const originalAttributes = model.rawAttributes;
|
||||
const virtualFields = {};
|
||||
@@ -520,72 +600,137 @@ const syncModelsAlways = async (models) => {
|
||||
const schema = model.options?.schema || 'public';
|
||||
|
||||
console.log(` 🔍 Checking for foreign keys in ${schema}.${tableName}...`);
|
||||
const foreignKeys = await sequelize.query(`
|
||||
// Verwende queryWithTimeout für Foreign Key Queries
|
||||
let foreignKeys = [];
|
||||
try {
|
||||
// Verwende direkte SQL-String-Interpolation, da Parameter-Binding bei queryWithTimeout Probleme macht
|
||||
const result = await queryWithTimeout(`
|
||||
SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_name = :tableName
|
||||
AND tc.table_schema = :schema
|
||||
`, {
|
||||
replacements: { tableName, schema },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
AND tc.table_name = '${tableName.replace(/'/g, "''")}'
|
||||
AND tc.table_schema = '${schema.replace(/'/g, "''")}'
|
||||
`, 10000, `FK check for ${model.name}`);
|
||||
// Result ist ein Array von Objekten
|
||||
foreignKeys = Array.isArray(result) ? result : [];
|
||||
} catch (fkCheckError) {
|
||||
if (fkCheckError.message && fkCheckError.message.includes('Timeout')) {
|
||||
console.warn(` ⚠️ Timeout beim Prüfen der Foreign Keys für ${model.name} - überspringe FK-Check...`);
|
||||
foreignKeys = [];
|
||||
} else {
|
||||
// Bei anderen Fehlern auch überspringen, nicht kritisch
|
||||
console.warn(` ⚠️ Fehler beim Prüfen der Foreign Keys für ${model.name}:`, fkCheckError.message?.substring(0, 100));
|
||||
foreignKeys = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (foreignKeys && foreignKeys.length > 0) {
|
||||
console.log(` ⚠️ Found ${foreignKeys.length} existing foreign keys:`, foreignKeys.map(fk => fk.constraint_name).join(', '));
|
||||
console.log(` ⚠️ Removing ${foreignKeys.length} existing foreign keys from ${model.name} (schema: ${schema}) before sync`);
|
||||
for (const fk of foreignKeys) {
|
||||
console.log(` 🗑️ Dropping constraint: ${fk.constraint_name}`);
|
||||
await sequelize.query(`
|
||||
const constraintNames = foreignKeys
|
||||
.map(fk => fk?.constraint_name)
|
||||
.filter(name => name && name !== 'undefined' && name !== undefined);
|
||||
|
||||
if (constraintNames.length > 0) {
|
||||
console.log(` ⚠️ Found ${constraintNames.length} existing foreign keys:`, constraintNames.join(', '));
|
||||
console.log(` ⚠️ Removing ${constraintNames.length} existing foreign keys from ${model.name} (schema: ${schema}) before sync`);
|
||||
for (const constraintName of constraintNames) {
|
||||
try {
|
||||
console.log(` 🗑️ Dropping constraint: ${constraintName}`);
|
||||
await queryWithTimeout(`
|
||||
ALTER TABLE "${schema}"."${tableName}"
|
||||
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
||||
`);
|
||||
DROP CONSTRAINT IF EXISTS "${constraintName.replace(/"/g, '""')}" CASCADE
|
||||
`, 10000, `Drop FK ${constraintName}`);
|
||||
} catch (dropError) {
|
||||
if (dropError.message && dropError.message.includes('Timeout')) {
|
||||
console.warn(` ⚠️ Timeout beim Entfernen von ${constraintName} - überspringe...`);
|
||||
} else {
|
||||
console.warn(` ⚠️ Konnte ${constraintName} nicht entfernen:`, dropError.message?.substring(0, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` ✅ Foreign key removal completed for ${model.name}`);
|
||||
} else {
|
||||
console.log(` ⚠️ Foreign keys gefunden, aber constraint_name ist undefined - überspringe FK-Entfernung`);
|
||||
}
|
||||
console.log(` ✅ All foreign keys removed for ${model.name}`);
|
||||
} else {
|
||||
console.log(` ✅ No foreign keys found for ${model.name}`);
|
||||
}
|
||||
} catch (fkError) {
|
||||
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message);
|
||||
console.warn(` ⚠️ Error details:`, fkError);
|
||||
// Ignoriere Timeout-Fehler - nicht kritisch
|
||||
if (fkError.message && fkError.message.includes('Timeout')) {
|
||||
console.warn(` ⚠️ Timeout beim Prüfen der Foreign Keys für ${model.name} - überspringe...`);
|
||||
} else {
|
||||
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message?.substring(0, 100));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` 🔄 Syncing model ${model.name} with constraints: false`);
|
||||
try {
|
||||
// Versuche doppelte pg_description Einträge vor dem Sync zu bereinigen
|
||||
// Hinweis: Benötigt Superuser-Rechte oder spezielle Berechtigungen
|
||||
try {
|
||||
// Überspringe pg_description Cleanup komplett - benötigt Superuser-Rechte und blockiert oft
|
||||
// Diese Query ist nicht kritisch für die Funktionalität
|
||||
|
||||
// Prüfe ob Tabelle bereits existiert - wenn ja, überspringe Sync für große Tabellen
|
||||
const tableName = model.tableName;
|
||||
const schema = model.options?.schema || 'public';
|
||||
// Verwende direkte Parameter-Einsetzung, da DO $$ keine Parameterbindung unterstützt
|
||||
// Die Parameter sind sicher, da sie von Sequelize-Modell-Eigenschaften kommen
|
||||
await sequelize.query(`
|
||||
DELETE FROM pg_catalog.pg_description d1
|
||||
WHERE d1.objoid IN (
|
||||
SELECT c.oid
|
||||
FROM pg_catalog.pg_class c
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relname = '${tableName.replace(/'/g, "''")}'
|
||||
AND n.nspname = '${schema.replace(/'/g, "''")}'
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_description d2
|
||||
WHERE d2.objoid = d1.objoid
|
||||
AND d2.objsubid = d1.objsubid
|
||||
AND d2.ctid < d1.ctid
|
||||
)
|
||||
`);
|
||||
} catch (descError) {
|
||||
// Ignoriere Berechtigungsfehler - das ist normal, wenn der Benutzer keine Superuser-Rechte hat
|
||||
if (descError.message && descError.message.includes('Berechtigung')) {
|
||||
console.log(` ℹ️ Cannot clean up duplicate pg_description entries (requires superuser privileges): ${model.name}`);
|
||||
} else {
|
||||
console.warn(` ⚠️ Could not clean up duplicate pg_description entries for ${model.name}:`, descError.message);
|
||||
}
|
||||
let tableExists = false;
|
||||
try {
|
||||
const existsResult = await queryWithTimeout(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = :schema
|
||||
AND table_name = :tableName
|
||||
) as exists
|
||||
`, 5000, `Table exists check for ${model.name}`, { schema, tableName });
|
||||
tableExists = existsResult && existsResult[0] && existsResult[0].exists;
|
||||
} catch (checkError) {
|
||||
// Bei Fehler annehmen, dass Tabelle nicht existiert und Sync versuchen
|
||||
tableExists = false;
|
||||
}
|
||||
|
||||
await model.sync({ alter: true, force: false, constraints: false });
|
||||
// Für große Tabellen (FalukantUser, FalukantCharacter) - wenn Tabelle existiert, überspringe Sync
|
||||
const largeTables = ['FalukantUser', 'FalukantCharacter', 'Notification', 'RegionData'];
|
||||
if (largeTables.includes(model.name) && tableExists) {
|
||||
console.log(` ℹ️ ${model.name} Tabelle existiert bereits - überspringe Sync (zu groß für alter)`);
|
||||
// Restore associations before continuing
|
||||
if (associationKeys.length > 0) {
|
||||
model.associations = originalAssociations;
|
||||
}
|
||||
// Restore virtual fields
|
||||
for (const [key, attr] of Object.entries(virtualFields)) {
|
||||
model.rawAttributes[key] = attr;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Für Tabellen mit problematischen ENUM-Änderungen - wenn Tabelle existiert, überspringe Sync
|
||||
// Sequelize generiert fehlerhaftes SQL bei ENUM-Änderungen mit Kommentaren
|
||||
const enumProblemTables = ['TaxiMapTileHouse'];
|
||||
if (enumProblemTables.includes(model.name) && tableExists) {
|
||||
console.log(` ℹ️ ${model.name} Tabelle existiert bereits - überspringe Sync (ENUM-Änderungen problematisch)`);
|
||||
// Restore associations before continuing
|
||||
if (associationKeys.length > 0) {
|
||||
model.associations = originalAssociations;
|
||||
}
|
||||
// Restore virtual fields
|
||||
for (const [key, attr] of Object.entries(virtualFields)) {
|
||||
model.rawAttributes[key] = attr;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verwende syncModelWithTimeout für große Tabellen
|
||||
const syncSuccess = await syncModelWithTimeout(model, 60000);
|
||||
if (!syncSuccess) {
|
||||
console.warn(` ⚠️ ${model.name} wurde übersprungen aufgrund von Timeout`);
|
||||
// Restore associations before continuing
|
||||
if (associationKeys.length > 0) {
|
||||
model.associations = originalAssociations;
|
||||
}
|
||||
// Restore virtual fields
|
||||
for (const [key, attr] of Object.entries(virtualFields)) {
|
||||
model.rawAttributes[key] = attr;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} catch (syncError) {
|
||||
// Wenn Sequelize einen "mehr als eine Zeile" Fehler hat, überspringe das Model
|
||||
// Dies kann durch doppelte pg_description Einträge oder mehrere Tabellen mit demselben Namen verursacht werden
|
||||
|
||||
@@ -1,6 +1,64 @@
|
||||
// syncDatabase.js
|
||||
|
||||
import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways, sequelize } from './sequelize.js';
|
||||
|
||||
// Helper: Query mit Timeout
|
||||
const queryWithTimeout = async (query, timeoutMs = 30000, description = 'Query') => {
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
sequelize.query(query),
|
||||
timeoutPromise
|
||||
]);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error.message.includes('Timeout')) {
|
||||
console.warn(`⚠️ ${description} hat Timeout nach ${timeoutMs}ms - überspringe...`);
|
||||
return [null, 0]; // Return empty result
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Retry wrapper for transient pool/connection issues
|
||||
const runWithRetry = async (fn, { retries = 3, delayMs = 2000, description = 'operation' } = {}) => {
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const isAcquireTimeout = error?.name === 'SequelizeConnectionAcquireTimeoutError'
|
||||
|| error?.message?.includes('ConnectionAcquireTimeoutError')
|
||||
|| error?.message?.includes('Operation timeout');
|
||||
if (!isAcquireTimeout || attempt === retries) {
|
||||
throw error;
|
||||
}
|
||||
console.warn(`⚠️ ${description} fehlgeschlagen (AcquireTimeout). Retry ${attempt}/${retries} in ${delayMs}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
};
|
||||
|
||||
// Helper: Prüft ob Tabelle existiert
|
||||
const tableExists = async (schema, tableName) => {
|
||||
try {
|
||||
const result = await sequelize.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = '${schema}'
|
||||
AND table_name = '${tableName}'
|
||||
);
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
return result[0]?.exists || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
import initializeTypes from './initializeTypes.js';
|
||||
import initializeSettings from './initializeSettings.js';
|
||||
import initializeUserRights from './initializeUserRights.js';
|
||||
@@ -14,6 +72,7 @@ import initializeChat from './initializeChat.js';
|
||||
import initializeMatch3Data from './initializeMatch3.js';
|
||||
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
|
||||
import initializeTaxi from './initializeTaxi.js';
|
||||
import initializeWidgetTypes from './initializeWidgetTypes.js';
|
||||
|
||||
// Normale Synchronisation (nur bei STAGE=dev Schema-Updates)
|
||||
const syncDatabase = async () => {
|
||||
@@ -33,6 +92,39 @@ const syncDatabase = async () => {
|
||||
console.log("Initializing database schemas...");
|
||||
await initializeDatabase();
|
||||
|
||||
// Dashboard: Widget-Typen-Tabelle (mögliche Widgets)
|
||||
console.log("Ensuring widget_type table exists...");
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS type.widget_type (
|
||||
id SERIAL PRIMARY KEY,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
endpoint VARCHAR(255) NOT NULL,
|
||||
description VARCHAR(255),
|
||||
order_id INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`);
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte type.widget_type nicht anlegen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Dashboard: Benutzer-Konfiguration (Widgets pro User)
|
||||
console.log("Ensuring user_dashboard table exists...");
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.user_dashboard (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
config JSONB NOT NULL DEFAULT '{"widgets":[]}'::jsonb,
|
||||
CONSTRAINT user_dashboard_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte community.user_dashboard nicht anlegen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations)
|
||||
// Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt.
|
||||
// Damit API/Menu nicht mit "relation does not exist" (42P01) scheitert, legen wir die Tabellen idempotent an.
|
||||
@@ -144,12 +236,330 @@ const syncDatabase = async () => {
|
||||
ON community.vocab_chapter_lexeme(learning_lexeme_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx
|
||||
ON community.vocab_chapter_lexeme(reference_lexeme_id);
|
||||
|
||||
// Kurs-Tabellen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course (
|
||||
id SERIAL PRIMARY KEY,
|
||||
owner_user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
language_id INTEGER NOT NULL,
|
||||
native_language_id INTEGER,
|
||||
difficulty_level INTEGER DEFAULT 1,
|
||||
is_public BOOLEAN DEFAULT false,
|
||||
share_code TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_owner_fk
|
||||
FOREIGN KEY (owner_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_language_fk
|
||||
FOREIGN KEY (language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_native_language_fk
|
||||
FOREIGN KEY (native_language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_lesson (
|
||||
id SERIAL PRIMARY KEY,
|
||||
course_id INTEGER NOT NULL,
|
||||
chapter_id INTEGER,
|
||||
lesson_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
week_number INTEGER,
|
||||
day_number INTEGER,
|
||||
lesson_type TEXT DEFAULT 'vocab',
|
||||
audio_url TEXT,
|
||||
cultural_notes TEXT,
|
||||
target_minutes INTEGER,
|
||||
target_score_percent INTEGER DEFAULT 80,
|
||||
requires_review BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_lesson_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_lesson_chapter_fk
|
||||
FOREIGN KEY (chapter_id)
|
||||
REFERENCES community.vocab_chapter(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
course_id INTEGER NOT NULL,
|
||||
enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_course_enrollment_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_enrollment_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_course_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
course_id INTEGER NOT NULL,
|
||||
lesson_id INTEGER NOT NULL,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
score INTEGER DEFAULT 0,
|
||||
last_accessed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT vocab_course_progress_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_course_fk
|
||||
FOREIGN KEY (course_id)
|
||||
REFERENCES community.vocab_course(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_lesson_fk
|
||||
FOREIGN KEY (lesson_id)
|
||||
REFERENCES community.vocab_course_lesson(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_owner_idx
|
||||
ON community.vocab_course(owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
|
||||
ON community.vocab_course(language_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx
|
||||
ON community.vocab_course(native_language_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
|
||||
ON community.vocab_course(is_public);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
|
||||
ON community.vocab_course_lesson(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx
|
||||
ON community.vocab_course_lesson(chapter_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx
|
||||
ON community.vocab_course_lesson(course_id, week_number);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx
|
||||
ON community.vocab_course_lesson(lesson_type);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx
|
||||
ON community.vocab_course_enrollment(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx
|
||||
ON community.vocab_course_enrollment(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx
|
||||
ON community.vocab_course_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx
|
||||
ON community.vocab_course_progress(course_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx
|
||||
ON community.vocab_course_progress(lesson_id);
|
||||
|
||||
// Grammatik-Übungstypen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
// Grammatik-Übungen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise (
|
||||
id SERIAL PRIMARY KEY,
|
||||
lesson_id INTEGER NOT NULL,
|
||||
exercise_type_id INTEGER NOT NULL,
|
||||
exercise_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
instruction TEXT,
|
||||
question_data JSONB NOT NULL,
|
||||
answer_data JSONB NOT NULL,
|
||||
explanation TEXT,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_grammar_exercise_lesson_fk
|
||||
FOREIGN KEY (lesson_id)
|
||||
REFERENCES community.vocab_course_lesson(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_type_fk
|
||||
FOREIGN KEY (exercise_type_id)
|
||||
REFERENCES community.vocab_grammar_exercise_type(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_creator_fk
|
||||
FOREIGN KEY (created_by_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number)
|
||||
);
|
||||
|
||||
// Fortschritt für Grammatik-Übungen
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
exercise_id INTEGER NOT NULL,
|
||||
attempts INTEGER DEFAULT 0,
|
||||
correct_attempts INTEGER DEFAULT 0,
|
||||
last_attempt_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
completed BOOLEAN DEFAULT false,
|
||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_exercise_fk
|
||||
FOREIGN KEY (exercise_id)
|
||||
REFERENCES community.vocab_grammar_exercise(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx
|
||||
ON community.vocab_grammar_exercise(lesson_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx
|
||||
ON community.vocab_grammar_exercise(exercise_type_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx
|
||||
ON community.vocab_grammar_exercise_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx
|
||||
ON community.vocab_grammar_exercise_progress(exercise_id);
|
||||
|
||||
-- Standard-Übungstypen einfügen
|
||||
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
|
||||
('gap_fill', 'Lückentext-Übung'),
|
||||
('multiple_choice', 'Multiple-Choice-Fragen'),
|
||||
('sentence_building', 'Satzbau-Übung'),
|
||||
('transformation', 'Satzumformung'),
|
||||
('conjugation', 'Konjugations-Übung'),
|
||||
('declension', 'Deklinations-Übung')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
`);
|
||||
console.log("✅ Vocab-Trainer Tabellen sind vorhanden.");
|
||||
console.log("✅ Vocab-Course Tabellen sind vorhanden.");
|
||||
console.log("✅ Vocab-Grammar-Exercise Tabellen sind vorhanden.");
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte Vocab-Trainer Tabellen nicht sicherstellen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Migration: ChurchApplication supervisor_id nullable machen (kritisch für Funktionalität)
|
||||
console.log("Making church_application supervisor_id nullable...");
|
||||
try {
|
||||
await sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Prüfe ob supervisor_id NOT NULL Constraint existiert
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'church_application'
|
||||
AND column_name = 'supervisor_id'
|
||||
AND is_nullable = 'NO'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.church_application
|
||||
ALTER COLUMN supervisor_id DROP NOT NULL;
|
||||
RAISE NOTICE 'supervisor_id NOT NULL Constraint entfernt';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
console.log("✅ church_application supervisor_id ist jetzt nullable");
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte church_application supervisor_id nicht nullable machen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Relationship-/Marriage-Proposal-Änderungen loggen (keine Einträge löschen; ohne db:migrate)
|
||||
console.log("Ensuring relationship change log (falukant) exists...");
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS falukant_log.relationship_change_log (
|
||||
id serial PRIMARY KEY,
|
||||
changed_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
table_name character varying(64) NOT NULL,
|
||||
operation character varying(16) NOT NULL,
|
||||
record_id integer,
|
||||
payload_old jsonb,
|
||||
payload_new jsonb
|
||||
);
|
||||
`);
|
||||
await sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS relationship_change_log_changed_at_idx
|
||||
ON falukant_log.relationship_change_log (changed_at);
|
||||
`);
|
||||
await sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS relationship_change_log_table_operation_idx
|
||||
ON falukant_log.relationship_change_log (table_name, operation);
|
||||
`);
|
||||
await sequelize.query(`
|
||||
CREATE OR REPLACE FUNCTION falukant_log.log_relationship_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_record_id INTEGER;
|
||||
v_payload_old JSONB;
|
||||
v_payload_new JSONB;
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
v_record_id := NEW.id;
|
||||
v_payload_old := NULL;
|
||||
v_payload_new := to_jsonb(NEW);
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
v_record_id := NEW.id;
|
||||
v_payload_old := to_jsonb(OLD);
|
||||
v_payload_new := to_jsonb(NEW);
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
v_record_id := OLD.id;
|
||||
v_payload_old := to_jsonb(OLD);
|
||||
v_payload_new := NULL;
|
||||
END IF;
|
||||
INSERT INTO falukant_log.relationship_change_log (
|
||||
table_name, operation, record_id, payload_old, payload_new
|
||||
) VALUES (
|
||||
TG_TABLE_NAME, TG_OP, v_record_id, v_payload_old, v_payload_new
|
||||
);
|
||||
IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
await sequelize.query(`
|
||||
DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.relationship;
|
||||
CREATE TRIGGER trg_log_relationship_change
|
||||
AFTER INSERT OR UPDATE OR DELETE ON falukant_data.relationship
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION falukant_log.log_relationship_change();
|
||||
`);
|
||||
await sequelize.query(`
|
||||
DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.marriage_proposals;
|
||||
CREATE TRIGGER trg_log_relationship_change
|
||||
AFTER INSERT OR UPDATE OR DELETE ON falukant_data.marriage_proposals
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION falukant_log.log_relationship_change();
|
||||
`);
|
||||
console.log("✅ relationship_change_log und Trigger sind vorhanden.");
|
||||
} catch (e) {
|
||||
console.warn('⚠️ relationship_change_log/Trigger konnten nicht sichergestellt werden:', e?.message || e);
|
||||
}
|
||||
|
||||
// Preishistorie für Produkte (Zeitreihe) – nur Schema/Struktur, noch ohne Logik
|
||||
console.log("Ensuring falukant_log.product_price_history exists...");
|
||||
try {
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS falukant_log.product_price_history (
|
||||
id serial PRIMARY KEY,
|
||||
product_id integer NOT NULL,
|
||||
region_id integer NOT NULL,
|
||||
price numeric(12,2) NOT NULL,
|
||||
recorded_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
await sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS product_price_history_product_region_recorded_idx
|
||||
ON falukant_log.product_price_history (product_id, region_id, recorded_at);
|
||||
`);
|
||||
console.log("✅ product_price_history ist vorhanden.");
|
||||
} catch (e) {
|
||||
console.warn('⚠️ product_price_history konnte nicht sichergestellt werden:', e?.message || e);
|
||||
}
|
||||
|
||||
// Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt
|
||||
console.log("Pre-ensure Taxi columns (traffic_light) ...");
|
||||
try {
|
||||
@@ -277,14 +687,17 @@ const syncDatabase = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Setting up associations...");
|
||||
setupAssociations();
|
||||
// Associations werden vom Aufrufer (z. B. server.js) vor dem Aufruf von syncDatabase() gesetzt.
|
||||
// Kein setupAssociations() hier, sonst doppelter Aufruf → AssociationError (alias "rooms" etc.).
|
||||
|
||||
console.log("Synchronizing models...");
|
||||
await syncModelsWithUpdates(models);
|
||||
|
||||
console.log("Initializing settings...");
|
||||
await initializeSettings();
|
||||
await runWithRetry(
|
||||
() => initializeSettings(),
|
||||
{ retries: 3, delayMs: 2000, description: 'initializeSettings' }
|
||||
);
|
||||
|
||||
console.log("Initializing types...");
|
||||
await initializeTypes();
|
||||
@@ -318,6 +731,9 @@ const syncDatabase = async () => {
|
||||
console.log("Initializing Taxi...");
|
||||
await initializeTaxi();
|
||||
|
||||
console.log("Initializing widget types...");
|
||||
await initializeWidgetTypes();
|
||||
|
||||
console.log('Database synchronization complete.');
|
||||
} catch (error) {
|
||||
console.error('Unable to synchronize the database:', error);
|
||||
@@ -399,89 +815,122 @@ const syncDatabaseForDeployment = async () => {
|
||||
console.warn('⚠️ Konnte Transport-Spalten nicht nullable machen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Migration: ChurchApplication supervisor_id nullable machen
|
||||
console.log("Making church_application supervisor_id nullable...");
|
||||
try {
|
||||
await sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Prüfe ob supervisor_id NOT NULL Constraint existiert
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'church_application'
|
||||
AND column_name = 'supervisor_id'
|
||||
AND is_nullable = 'NO'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.church_application
|
||||
ALTER COLUMN supervisor_id DROP NOT NULL;
|
||||
RAISE NOTICE 'supervisor_id NOT NULL Constraint entfernt';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
console.log("✅ church_application supervisor_id ist jetzt nullable");
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Konnte church_application supervisor_id nicht nullable machen:', e?.message || e);
|
||||
}
|
||||
|
||||
// Cleanup: Entferne verwaiste Einträge vor Schema-Updates
|
||||
console.log("Cleaning up orphaned entries...");
|
||||
try {
|
||||
// Cleanup user_param_visibility
|
||||
const result1 = await sequelize.query(`
|
||||
// Cleanup user_param_visibility (optimiert mit LEFT JOIN)
|
||||
console.log(" → Prüfe user_param_visibility...");
|
||||
const result1 = await queryWithTimeout(`
|
||||
DELETE FROM community.user_param_visibility
|
||||
WHERE param_id NOT IN (
|
||||
SELECT id FROM community.user_param
|
||||
);
|
||||
`);
|
||||
`, 30000, 'user_param_visibility cleanup');
|
||||
const deletedCount1 = result1[1] || 0;
|
||||
if (deletedCount1 > 0) {
|
||||
console.log(`✅ ${deletedCount1} verwaiste user_param_visibility Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup stock mit ungültigen branch_id (0 oder nicht existierend)
|
||||
const result2 = await sequelize.query(`
|
||||
console.log(" → Prüfe stock...");
|
||||
const result2 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_data.stock
|
||||
WHERE branch_id = 0 OR branch_id NOT IN (
|
||||
SELECT id FROM falukant_data.branch
|
||||
);
|
||||
`);
|
||||
`, 30000, 'stock cleanup');
|
||||
const deletedCount2 = result2[1] || 0;
|
||||
if (deletedCount2 > 0) {
|
||||
console.log(`✅ ${deletedCount2} verwaiste stock Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup knowledge mit ungültigen character_id oder product_id
|
||||
const result3 = await sequelize.query(`
|
||||
console.log(" → Prüfe knowledge...");
|
||||
const result3 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_data.knowledge
|
||||
WHERE character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR product_id NOT IN (
|
||||
SELECT id FROM falukant_type.product
|
||||
);
|
||||
`);
|
||||
`, 30000, 'knowledge cleanup');
|
||||
const deletedCount3 = result3[1] || 0;
|
||||
if (deletedCount3 > 0) {
|
||||
console.log(`✅ ${deletedCount3} verwaiste knowledge Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup notification mit ungültigen user_id
|
||||
const result4 = await sequelize.query(`
|
||||
console.log(" → Prüfe notification...");
|
||||
const result4 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_log.notification
|
||||
WHERE user_id NOT IN (
|
||||
SELECT id FROM falukant_data.falukant_user
|
||||
);
|
||||
`);
|
||||
`, 30000, 'notification cleanup');
|
||||
const deletedCount4 = result4[1] || 0;
|
||||
if (deletedCount4 > 0) {
|
||||
console.log(`✅ ${deletedCount4} verwaiste notification Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id
|
||||
const result5 = await sequelize.query(`
|
||||
console.log(" → Prüfe promotional_gift...");
|
||||
const result5 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_log.promotional_gift
|
||||
WHERE sender_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR recipient_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
);
|
||||
`);
|
||||
`, 30000, 'promotional_gift cleanup');
|
||||
const deletedCount5 = result5[1] || 0;
|
||||
if (deletedCount5 > 0) {
|
||||
console.log(`✅ ${deletedCount5} verwaiste promotional_gift Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup user_house mit ungültigen house_type_id oder user_id
|
||||
const result6 = await sequelize.query(`
|
||||
console.log(" → Prüfe user_house...");
|
||||
const result6 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_data.user_house
|
||||
WHERE house_type_id NOT IN (
|
||||
SELECT id FROM falukant_type.house
|
||||
) OR user_id NOT IN (
|
||||
SELECT id FROM falukant_data.falukant_user
|
||||
);
|
||||
`);
|
||||
`, 30000, 'user_house cleanup');
|
||||
const deletedCount6 = result6[1] || 0;
|
||||
if (deletedCount6 > 0) {
|
||||
console.log(`✅ ${deletedCount6} verwaiste user_house Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id
|
||||
const result7 = await sequelize.query(`
|
||||
console.log(" → Prüfe child_relation...");
|
||||
const result7 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_data.child_relation
|
||||
WHERE father_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
@@ -490,14 +939,15 @@ const syncDatabaseForDeployment = async () => {
|
||||
) OR child_character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
);
|
||||
`);
|
||||
`, 30000, 'child_relation cleanup');
|
||||
const deletedCount7 = result7[1] || 0;
|
||||
if (deletedCount7 > 0) {
|
||||
console.log(`✅ ${deletedCount7} verwaiste child_relation Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup political_office mit ungültigen character_id, office_type_id oder region_id
|
||||
const result8 = await sequelize.query(`
|
||||
console.log(" → Prüfe political_office...");
|
||||
const result8 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_data.political_office
|
||||
WHERE character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
@@ -506,33 +956,74 @@ const syncDatabaseForDeployment = async () => {
|
||||
) OR region_id NOT IN (
|
||||
SELECT id FROM falukant_data.region
|
||||
);
|
||||
`);
|
||||
`, 30000, 'political_office cleanup');
|
||||
const deletedCount8 = result8[1] || 0;
|
||||
if (deletedCount8 > 0) {
|
||||
console.log(`✅ ${deletedCount8} verwaiste political_office Einträge entfernt`);
|
||||
}
|
||||
|
||||
// Cleanup church_office mit ungültigen character_id, office_type_id oder region_id (nur wenn Tabelle existiert)
|
||||
if (await tableExists('falukant_data', 'church_office')) {
|
||||
console.log(" → Prüfe church_office...");
|
||||
const result11 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_data.church_office
|
||||
WHERE character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR office_type_id NOT IN (
|
||||
SELECT id FROM falukant_type.church_office_type
|
||||
) OR region_id NOT IN (
|
||||
SELECT id FROM falukant_data.region
|
||||
);
|
||||
`, 30000, 'church_office cleanup');
|
||||
const deletedCount11 = result11[1] || 0;
|
||||
if (deletedCount11 > 0) {
|
||||
console.log(`✅ ${deletedCount11} verwaiste church_office Einträge entfernt`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup church_application mit ungültigen character_id, office_type_id, region_id oder supervisor_id (nur wenn Tabelle existiert)
|
||||
if (await tableExists('falukant_data', 'church_application')) {
|
||||
console.log(" → Prüfe church_application...");
|
||||
const result12 = await queryWithTimeout(`
|
||||
DELETE FROM falukant_data.church_application
|
||||
WHERE character_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
) OR office_type_id NOT IN (
|
||||
SELECT id FROM falukant_type.church_office_type
|
||||
) OR region_id NOT IN (
|
||||
SELECT id FROM falukant_data.region
|
||||
) OR (supervisor_id IS NOT NULL AND supervisor_id NOT IN (
|
||||
SELECT id FROM falukant_data.character
|
||||
));
|
||||
`, 30000, 'church_application cleanup');
|
||||
const deletedCount12 = result12[1] || 0;
|
||||
if (deletedCount12 > 0) {
|
||||
console.log(`✅ ${deletedCount12} verwaiste church_application Einträge entfernt`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup vehicle.condition: Legacy-Nulls + Range clamp (UI zeigt sonst "Unbekannt")
|
||||
const result9 = await sequelize.query(`
|
||||
console.log(" → Prüfe vehicle.condition...");
|
||||
const result9 = await queryWithTimeout(`
|
||||
UPDATE falukant_data.vehicle
|
||||
SET condition = 100
|
||||
WHERE condition IS NULL;
|
||||
`);
|
||||
`, 30000, 'vehicle condition NULL update');
|
||||
const updatedNullConditions = result9[1] || 0;
|
||||
if (updatedNullConditions > 0) {
|
||||
console.log(`✅ ${updatedNullConditions} vehicle.condition NULL → 100 gesetzt`);
|
||||
}
|
||||
const result10 = await sequelize.query(`
|
||||
const result10 = await queryWithTimeout(`
|
||||
UPDATE falukant_data.vehicle
|
||||
SET condition = GREATEST(0, LEAST(100, condition))
|
||||
WHERE condition < 0 OR condition > 100;
|
||||
`);
|
||||
`, 30000, 'vehicle condition clamp');
|
||||
const clampedConditions = result10[1] || 0;
|
||||
if (clampedConditions > 0) {
|
||||
console.log(`✅ ${clampedConditions} vehicle.condition Werte auf 0..100 geklemmt`);
|
||||
}
|
||||
|
||||
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0 && updatedNullConditions === 0 && clampedConditions === 0) {
|
||||
if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0 && deletedCount11 === 0 && deletedCount12 === 0 && updatedNullConditions === 0 && clampedConditions === 0) {
|
||||
console.log("✅ Keine verwaisten Einträge gefunden");
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -580,6 +1071,9 @@ const syncDatabaseForDeployment = async () => {
|
||||
console.log("Initializing Taxi...");
|
||||
await initializeTaxi();
|
||||
|
||||
console.log("Initializing widget types...");
|
||||
await initializeWidgetTypes();
|
||||
|
||||
console.log('Database synchronization for deployment complete.');
|
||||
} catch (error) {
|
||||
console.error('Unable to synchronize the database for deployment:', error);
|
||||
|
||||
113
backend/vacuum-database.js
Executable file
113
backend/vacuum-database.js
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script zum Ausführen von VACUUM auf Tabellen mit hohem Bloat
|
||||
*
|
||||
* Führt VACUUM ANALYZE auf Tabellen aus, die:
|
||||
* - Hohen Bloat-Anteil haben (> 20% tote Zeilen)
|
||||
* - Seit mehr als 7 Tagen nicht gevacuumt wurden
|
||||
* - Viele tote Zeilen haben (> 1000)
|
||||
*/
|
||||
|
||||
import './config/loadEnv.js';
|
||||
import { sequelize } from './utils/sequelize.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🧹 Datenbank-Vacuum\n');
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
// Finde Tabellen, die Vacuum benötigen
|
||||
const [tablesToVacuum] = await sequelize.query(`
|
||||
SELECT
|
||||
schemaname || '.' || relname as table_name,
|
||||
n_dead_tup,
|
||||
n_live_tup,
|
||||
CASE
|
||||
WHEN n_live_tup > 0 THEN round((n_dead_tup::numeric / n_live_tup::numeric) * 100, 2)
|
||||
ELSE 0
|
||||
END as dead_percent,
|
||||
last_vacuum,
|
||||
last_autovacuum,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as table_size
|
||||
FROM pg_stat_user_tables
|
||||
WHERE schemaname IN ('falukant_data', 'falukant_type', 'community', 'logs')
|
||||
AND (
|
||||
(n_dead_tup > 1000 AND (n_live_tup = 0 OR (n_dead_tup::numeric / NULLIF(n_live_tup, 0)) > 0.2))
|
||||
OR (last_vacuum IS NULL AND last_autovacuum IS NULL AND n_dead_tup > 0)
|
||||
OR (
|
||||
(last_vacuum < now() - interval '7 days' OR last_vacuum IS NULL)
|
||||
AND (last_autovacuum < now() - interval '7 days' OR last_autovacuum IS NULL)
|
||||
AND n_dead_tup > 100
|
||||
)
|
||||
)
|
||||
ORDER BY n_dead_tup DESC;
|
||||
`);
|
||||
|
||||
if (tablesToVacuum.length === 0) {
|
||||
console.log('✅ Keine Tabellen benötigen Vacuum.\n');
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`📋 Gefunden: ${tablesToVacuum.length} Tabellen benötigen Vacuum\n`);
|
||||
|
||||
// Zeige Tabellen
|
||||
console.log('Tabellen, die gevacuumt werden:');
|
||||
tablesToVacuum.forEach((t, i) => {
|
||||
console.log(` ${i + 1}. ${t.table_name}`);
|
||||
console.log(` Größe: ${t.table_size}, Tote Zeilen: ${parseInt(t.n_dead_tup).toLocaleString()} (${t.dead_percent}%)`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Frage Bestätigung
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const answer = await new Promise((resolve) => {
|
||||
rl.question('Möchtest du VACUUM ANALYZE auf diesen Tabellen ausführen? (j/n): ', resolve);
|
||||
});
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() !== 'j' && answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'ja' && answer.toLowerCase() !== 'yes') {
|
||||
console.log('❌ Abgebrochen.\n');
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('\n🧹 Starte Vacuum...\n');
|
||||
|
||||
// Führe VACUUM ANALYZE aus
|
||||
for (let i = 0; i < tablesToVacuum.length; i++) {
|
||||
const table = tablesToVacuum[i];
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log(`[${i + 1}/${tablesToVacuum.length}] Vacuuming ${table.table_name}...`);
|
||||
|
||||
await sequelize.query(`VACUUM ANALYZE ${table.table_name};`);
|
||||
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(` ✅ Fertig in ${duration}s\n`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ Fehler: ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('✅ Vacuum abgeschlossen\n');
|
||||
|
||||
await sequelize.close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
78
build-local.sh
Executable file
78
build-local.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# YourPart Daemon Local Build Script für OpenSUSE Tumbleweed
|
||||
# Führen Sie dieses Script lokal auf Ihrem Entwicklungsrechner aus
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Farben für Output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_info "Starte lokalen Build für YourPart Daemon auf OpenSUSE Tumbleweed..."
|
||||
|
||||
# Prüfe ob wir im richtigen Verzeichnis sind
|
||||
if [ ! -f "CMakeLists.txt" ] || [ ! -f "daemon.conf" ]; then
|
||||
log_error "Bitte führen Sie dieses Script aus dem Projektverzeichnis aus!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prüfe Dependencies
|
||||
log_info "Prüfe Dependencies..."
|
||||
if ! command -v cmake &> /dev/null; then
|
||||
log_error "CMake nicht gefunden. Führen Sie zuerst install-dependencies-opensuse.sh aus!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v g++ &> /dev/null; then
|
||||
log_error "G++ nicht gefunden. Führen Sie zuerst install-dependencies-opensuse.sh aus!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Erstelle Build-Verzeichnis
|
||||
log_info "Erstelle Build-Verzeichnis..."
|
||||
if [ ! -d "build" ]; then
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
cd build
|
||||
|
||||
# Konfiguriere CMake
|
||||
log_info "Konfiguriere CMake..."
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Kompiliere
|
||||
log_info "Kompiliere Projekt..."
|
||||
make -j$(nproc)
|
||||
|
||||
cd ..
|
||||
|
||||
log_success "Lokaler Build abgeschlossen!"
|
||||
log_info ""
|
||||
log_info "Build-Ergebnisse:"
|
||||
log_info "- Binärdatei: build/yourpart-daemon"
|
||||
log_info "- Größe: $(du -h build/yourpart-daemon | cut -f1)"
|
||||
log_info ""
|
||||
log_info "Nächste Schritte:"
|
||||
log_info "1. Testen Sie die Binärdatei lokal"
|
||||
log_info "2. Deployen Sie auf den Server mit deploy.sh"
|
||||
log_info "3. Oder verwenden Sie deploy-server.sh direkt auf dem Server"
|
||||
45
check-apache-websocket.sh
Executable file
45
check-apache-websocket.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== Apache WebSocket-Konfiguration prüfen ==="
|
||||
echo ""
|
||||
|
||||
# Prüfe, welche Module aktiviert sind
|
||||
echo "Aktivierte Apache-Module:"
|
||||
apache2ctl -M 2>/dev/null | grep -E "(proxy|rewrite|ssl|headers)" || echo "Keine relevanten Module gefunden"
|
||||
echo ""
|
||||
|
||||
# Prüfe, ob die benötigten Module aktiviert sind
|
||||
REQUIRED_MODULES=("proxy" "proxy_http" "proxy_wstunnel" "rewrite" "ssl" "headers")
|
||||
MISSING_MODULES=()
|
||||
|
||||
for module in "${REQUIRED_MODULES[@]}"; do
|
||||
if ! apache2ctl -M 2>/dev/null | grep -q "${module}_module"; then
|
||||
MISSING_MODULES+=("$module")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING_MODULES[@]} -eq 0 ]; then
|
||||
echo "✅ Alle benötigten Module sind aktiviert"
|
||||
else
|
||||
echo "❌ Fehlende Module:"
|
||||
for module in "${MISSING_MODULES[@]}"; do
|
||||
echo " - $module"
|
||||
done
|
||||
echo ""
|
||||
echo "Aktivieren mit:"
|
||||
for module in "${MISSING_MODULES[@]}"; do
|
||||
echo " sudo a2enmod $module"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Apache-Konfiguration testen ==="
|
||||
if sudo apache2ctl configtest 2>&1; then
|
||||
echo "✅ Apache-Konfiguration ist gültig"
|
||||
else
|
||||
echo "❌ Apache-Konfiguration hat Fehler"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Aktive VirtualHosts ==="
|
||||
apache2ctl -S 2>/dev/null | grep -E "(443|4443|4551)" || echo "Keine relevanten VirtualHosts gefunden"
|
||||
33
check-lesson-exercises.sh
Executable file
33
check-lesson-exercises.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Script zum Prüfen, ob Übungen für eine Lektion vorhanden sind
|
||||
# Verwendung: ./check-lesson-exercises.sh [lesson_id]
|
||||
|
||||
LESSON_ID="${1:-1}"
|
||||
|
||||
echo "🔍 Prüfe Übungen für Lektion ID: $LESSON_ID"
|
||||
echo ""
|
||||
|
||||
psql -U yourpart -d yp3 << EOF
|
||||
-- Prüfe Lektion
|
||||
SELECT
|
||||
l.id,
|
||||
l.title,
|
||||
l.course_id,
|
||||
COUNT(e.id) as exercise_count
|
||||
FROM community.vocab_course_lesson l
|
||||
LEFT JOIN community.vocab_grammar_exercise e ON e.lesson_id = l.id
|
||||
WHERE l.id = $LESSON_ID
|
||||
GROUP BY l.id, l.title, l.course_id;
|
||||
|
||||
-- Zeige alle Übungen für diese Lektion
|
||||
SELECT
|
||||
e.id,
|
||||
e.exercise_number,
|
||||
e.title,
|
||||
e.exercise_type_id,
|
||||
et.name as exercise_type_name
|
||||
FROM community.vocab_grammar_exercise e
|
||||
LEFT JOIN community.vocab_grammar_exercise_type et ON et.id = e.exercise_type_id
|
||||
WHERE e.lesson_id = $LESSON_ID
|
||||
ORDER BY e.exercise_number;
|
||||
EOF
|
||||
38
check-vocab-schema.sql
Normal file
38
check-vocab-schema.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- ============================================
|
||||
-- Prüfe ob alle notwendigen Spalten vorhanden sind
|
||||
-- ============================================
|
||||
-- Führe diese Queries auf dem Server aus, um zu prüfen, ob alles vorhanden ist
|
||||
|
||||
-- Prüfe native_language_id in vocab_course
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'community'
|
||||
AND table_name = 'vocab_course'
|
||||
AND column_name = 'native_language_id';
|
||||
|
||||
-- Prüfe cultural_notes in vocab_course_lesson
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'community'
|
||||
AND table_name = 'vocab_course_lesson'
|
||||
AND column_name = 'cultural_notes';
|
||||
|
||||
-- Prüfe ob vocab_grammar_exercise Tabelle existiert
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'community'
|
||||
AND table_name = 'vocab_grammar_exercise'
|
||||
);
|
||||
|
||||
-- Prüfe ob vocab_grammar_exercise_type Tabelle existiert
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'community'
|
||||
AND table_name = 'vocab_grammar_exercise_type'
|
||||
);
|
||||
53
check-websocket-services.sh
Executable file
53
check-websocket-services.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== WebSocket-Services Diagnose ==="
|
||||
echo ""
|
||||
|
||||
echo "1. Prüfe Backend-Service-Status:"
|
||||
sudo systemctl status yourpart-backend --no-pager -l | head -20
|
||||
echo ""
|
||||
|
||||
echo "2. Prüfe Daemon-Service-Status:"
|
||||
sudo systemctl status yourpart-daemon --no-pager -l | head -20
|
||||
echo ""
|
||||
|
||||
echo "3. Prüfe Backend-Logs (letzte 30 Zeilen):"
|
||||
sudo journalctl -u yourpart-backend -n 30 --no-pager | grep -E "(Socket|HTTPS|TLS|Port|4443|2020)" || echo "Keine relevanten Logs gefunden"
|
||||
echo ""
|
||||
|
||||
echo "4. Prüfe Daemon-Logs (letzte 30 Zeilen):"
|
||||
sudo journalctl -u yourpart-daemon -n 30 --no-pager | grep -E "(Daemon|WSS|TLS|Port|4551|connection)" || echo "Keine relevanten Logs gefunden"
|
||||
echo ""
|
||||
|
||||
echo "5. Prüfe, welche Ports lauschen:"
|
||||
echo " Port 2020 (API):"
|
||||
sudo netstat -tlnp 2>/dev/null | grep ":2020 " || echo " ❌ Port 2020 lauscht nicht"
|
||||
echo " Port 4443 (Socket.io):"
|
||||
sudo netstat -tlnp 2>/dev/null | grep ":4443 " || echo " ❌ Port 4443 lauscht nicht"
|
||||
echo " Port 4551 (Daemon):"
|
||||
sudo netstat -tlnp 2>/dev/null | grep ":4551 " || echo " ❌ Port 4551 lauscht nicht"
|
||||
echo ""
|
||||
|
||||
echo "6. Prüfe Firewall-Regeln:"
|
||||
sudo ufw status | grep -E "(4443|4551)" || echo " Keine Firewall-Regeln für diese Ports gefunden"
|
||||
echo ""
|
||||
|
||||
echo "7. Prüfe Backend-Umgebungsvariablen:"
|
||||
sudo systemctl show yourpart-backend --property=Environment --no-pager | grep -E "(SOCKET_IO|TLS)" || echo " Keine Socket.io TLS-Variablen gefunden"
|
||||
echo ""
|
||||
|
||||
echo "8. Prüfe Daemon-Umgebungsvariablen:"
|
||||
sudo systemctl show yourpart-daemon --property=Environment --no-pager | grep -E "(DAEMON|TLS)" || echo " Keine Daemon TLS-Variablen gefunden"
|
||||
echo ""
|
||||
|
||||
echo "9. Teste TLS-Zertifikate:"
|
||||
if [ -f "/etc/letsencrypt/live/www.your-part.de/privkey.pem" ]; then
|
||||
echo " ✅ Privkey gefunden"
|
||||
else
|
||||
echo " ❌ Privkey nicht gefunden"
|
||||
fi
|
||||
if [ -f "/etc/letsencrypt/live/www.your-part.de/fullchain.pem" ]; then
|
||||
echo " ✅ Fullchain gefunden"
|
||||
else
|
||||
echo " ❌ Fullchain nicht gefunden"
|
||||
fi
|
||||
175
cmake/install-config.cmake
Normal file
175
cmake/install-config.cmake
Normal file
@@ -0,0 +1,175 @@
|
||||
# CMake-Skript für intelligente Konfigurationsdatei-Installation
|
||||
# Fügt nur fehlende Keys hinzu, ohne bestehende Konfiguration zu überschreiben
|
||||
|
||||
# Pfade setzen
|
||||
set(CONFIG_FILE "/etc/yourpart/daemon.conf")
|
||||
set(TEMPLATE_FILE "/etc/yourpart/daemon.conf.example")
|
||||
|
||||
# Prüfe ob Template existiert (wurde von CMake installiert)
|
||||
if(NOT EXISTS "${TEMPLATE_FILE}")
|
||||
# Fallback 1: Versuche Template im Source-Verzeichnis zu finden
|
||||
# CMAKE_CURRENT_LIST_DIR zeigt auf cmake/ während der Installation
|
||||
get_filename_component(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
set(TEMPLATE_FILE_FALLBACK "${PROJECT_ROOT}/daemon.conf")
|
||||
|
||||
# Fallback 2: Versuche über CMAKE_SOURCE_DIR (falls verfügbar)
|
||||
if(DEFINED CMAKE_SOURCE_DIR AND EXISTS "${CMAKE_SOURCE_DIR}/daemon.conf")
|
||||
set(TEMPLATE_FILE "${CMAKE_SOURCE_DIR}/daemon.conf")
|
||||
message(STATUS "Verwende Template aus CMAKE_SOURCE_DIR: ${TEMPLATE_FILE}")
|
||||
elseif(EXISTS "${TEMPLATE_FILE_FALLBACK}")
|
||||
set(TEMPLATE_FILE "${TEMPLATE_FILE_FALLBACK}")
|
||||
message(STATUS "Verwende Template aus Source-Verzeichnis: ${TEMPLATE_FILE}")
|
||||
else()
|
||||
message(FATAL_ERROR "Template-Datei nicht gefunden!")
|
||||
message(FATAL_ERROR " Gesucht in: ${TEMPLATE_FILE}")
|
||||
message(FATAL_ERROR " Fallback 1: ${TEMPLATE_FILE_FALLBACK}")
|
||||
if(DEFINED CMAKE_SOURCE_DIR)
|
||||
message(FATAL_ERROR " Fallback 2: ${CMAKE_SOURCE_DIR}/daemon.conf")
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Verwende installierte Template-Datei: ${TEMPLATE_FILE}")
|
||||
endif()
|
||||
|
||||
# Prüfe ob Ziel-Verzeichnis existiert
|
||||
if(NOT EXISTS "/etc/yourpart")
|
||||
message(STATUS "Erstelle Verzeichnis /etc/yourpart...")
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "/etc/yourpart"
|
||||
RESULT_VARIABLE MKDIR_RESULT
|
||||
)
|
||||
if(NOT MKDIR_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Konnte Verzeichnis /etc/yourpart nicht erstellen")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Prüfe ob Config-Datei existiert
|
||||
if(NOT EXISTS "${CONFIG_FILE}")
|
||||
message(STATUS "Konfigurationsdatei existiert nicht, erstelle neue...")
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${TEMPLATE_FILE}" "${CONFIG_FILE}"
|
||||
RESULT_VARIABLE COPY_RESULT
|
||||
)
|
||||
if(NOT COPY_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Konnte Konfigurationsdatei nicht erstellen: ${CONFIG_FILE}")
|
||||
endif()
|
||||
message(STATUS "Neue Konfigurationsdatei erstellt: ${CONFIG_FILE}")
|
||||
else()
|
||||
message(STATUS "Konfigurationsdatei existiert bereits, prüfe auf fehlende Keys...")
|
||||
|
||||
# Verwende ein Python-Skript für intelligentes Merging
|
||||
# (CMake hat keine gute Unterstützung für komplexe String-Manipulation)
|
||||
# Erstelle temporäres Python-Skript im Build-Verzeichnis
|
||||
set(MERGE_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/merge-config.py")
|
||||
|
||||
# Erstelle Python-Skript
|
||||
file(WRITE "${MERGE_SCRIPT}"
|
||||
"#!/usr/bin/env python3
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
def merge_config(template_file, config_file):
|
||||
\"\"\"Fügt fehlende Keys aus Template zur Config hinzu, ohne bestehende zu überschreiben\"\"\"
|
||||
|
||||
# Lese bestehende Config
|
||||
existing_keys = {}
|
||||
existing_lines = []
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r') as f:
|
||||
for line in f:
|
||||
existing_lines.append(line.rstrip())
|
||||
# Extrahiere Key=Value Paare
|
||||
match = re.match(r'^\\s*([^#=]+?)\\s*=\\s*(.+?)\\s*$', line)
|
||||
if match:
|
||||
key = match.group(1).strip()
|
||||
value = match.group(2).strip()
|
||||
existing_keys[key] = value
|
||||
|
||||
# Lese Template
|
||||
new_keys = {}
|
||||
if not os.path.exists(template_file):
|
||||
print(f'Fehler: Template-Datei {template_file} nicht gefunden!', file=sys.stderr)
|
||||
return False
|
||||
|
||||
with open(template_file, 'r') as f:
|
||||
for line in f:
|
||||
# Extrahiere Key=Value Paare
|
||||
match = re.match(r'^\\s*([^#=]+?)\\s*=\\s*(.+?)\\s*$', line)
|
||||
if match:
|
||||
key = match.group(1).strip()
|
||||
value = match.group(2).strip()
|
||||
new_keys[key] = value
|
||||
|
||||
# Füge fehlende Keys hinzu
|
||||
added_count = 0
|
||||
for key, value in new_keys.items():
|
||||
if key not in existing_keys:
|
||||
existing_lines.append(f'{key}={value}')
|
||||
print(f'Füge fehlenden Key hinzu: {key}')
|
||||
added_count += 1
|
||||
|
||||
# Schreibe aktualisierte Config
|
||||
if added_count > 0:
|
||||
with open(config_file, 'w') as f:
|
||||
for line in existing_lines:
|
||||
f.write(line + '\\n')
|
||||
print(f'{added_count} neue Keys hinzugefügt')
|
||||
else:
|
||||
print('Keine neuen Keys hinzugefügt - Konfiguration ist aktuell')
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print('Verwendung: merge-config.py <template> <config>', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
template_file = sys.argv[1]
|
||||
config_file = sys.argv[2]
|
||||
|
||||
if not merge_config(template_file, config_file):
|
||||
sys.exit(1)
|
||||
")
|
||||
|
||||
# Setze Ausführungsrechte
|
||||
file(CHMOD "${MERGE_SCRIPT}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||
|
||||
# Führe Merge-Skript aus
|
||||
execute_process(
|
||||
COMMAND python3 "${MERGE_SCRIPT}" "${TEMPLATE_FILE}" "${CONFIG_FILE}"
|
||||
RESULT_VARIABLE MERGE_RESULT
|
||||
OUTPUT_VARIABLE MERGE_OUTPUT
|
||||
ERROR_VARIABLE MERGE_ERROR
|
||||
)
|
||||
|
||||
if(NOT MERGE_RESULT EQUAL 0)
|
||||
message(WARNING "Fehler beim Mergen der Config: ${MERGE_ERROR}")
|
||||
else()
|
||||
message(STATUS "${MERGE_OUTPUT}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Setze korrekte Berechtigungen (Fehler werden ignoriert, da Berechtigungen optional sind)
|
||||
execute_process(
|
||||
COMMAND chown yourpart:yourpart "${CONFIG_FILE}"
|
||||
RESULT_VARIABLE CHOWN_RESULT
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
if(NOT CHOWN_RESULT EQUAL 0)
|
||||
message(WARNING "Konnte Besitzer von ${CONFIG_FILE} nicht ändern (möglicherweise kein Root oder User existiert nicht)")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND chmod 600 "${CONFIG_FILE}"
|
||||
RESULT_VARIABLE CHMOD_RESULT
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
if(NOT CHMOD_RESULT EQUAL 0)
|
||||
message(WARNING "Konnte Berechtigungen von ${CONFIG_FILE} nicht ändern")
|
||||
endif()
|
||||
|
||||
message(STATUS "Konfigurationsdatei-Verwaltung abgeschlossen: ${CONFIG_FILE}")
|
||||
|
||||
10
daemon.conf
Normal file
10
daemon.conf
Normal file
@@ -0,0 +1,10 @@
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=yp3
|
||||
DB_USER=yourpart
|
||||
DB_PASSWORD=hitomisan
|
||||
THREAD_COUNT=4
|
||||
WEBSOCKET_PORT=4551
|
||||
WEBSOCKET_SSL_ENABLED=false
|
||||
WEBSOCKET_SSL_CERT_PATH=/home/torsten/Programs/yourpart-daemon/ssl-certs/server.crt
|
||||
WEBSOCKET_SSL_KEY_PATH=/home/torsten/Programs/yourpart-daemon/ssl-certs/server.key
|
||||
5
daemon.log
Normal file
5
daemon.log
Normal file
@@ -0,0 +1,5 @@
|
||||
WebSocket Server starting on port 4551 (no SSL)
|
||||
[2025/09/29 08:50:10:6854] N: lws_create_context: LWS: 4.3.5-unknown, NET CLI SRV H1 H2 WS ConMon IPv6-absent
|
||||
[2025/09/29 08:50:10:6874] N: __lws_lc_tag: ++ [wsi|0|pipe] (1)
|
||||
[2025/09/29 08:50:10:6874] N: __lws_lc_tag: ++ [vh|0|netlink] (1)
|
||||
WebSocket-Server erfolgreich gestartet auf Port 4551
|
||||
78
debug-lesson-exercises.js
Executable file
78
debug-lesson-exercises.js
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Debug-Script zum Prüfen, ob Übungen für eine Lektion vorhanden sind
|
||||
* Verwendung: node debug-lesson-exercises.js [lessonId]
|
||||
*/
|
||||
|
||||
import { sequelize } from './backend/utils/sequelize.js';
|
||||
import VocabCourseLesson from './backend/models/community/vocab_course_lesson.js';
|
||||
import VocabGrammarExercise from './backend/models/community/vocab_grammar_exercise.js';
|
||||
|
||||
const lessonId = process.argv[2] ? parseInt(process.argv[2]) : 1;
|
||||
|
||||
async function debugLesson() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Datenbankverbindung erfolgreich\n');
|
||||
|
||||
// Lade Lektion mit Übungen (wie im Backend)
|
||||
const lesson = await VocabCourseLesson.findByPk(lessonId, {
|
||||
include: [
|
||||
{
|
||||
model: require('./backend/models/community/vocab_course.js').default,
|
||||
as: 'course'
|
||||
},
|
||||
{
|
||||
model: VocabGrammarExercise,
|
||||
as: 'grammarExercises',
|
||||
include: [
|
||||
{
|
||||
model: require('./backend/models/community/vocab_grammar_exercise_type.js').default,
|
||||
as: 'exerciseType'
|
||||
}
|
||||
],
|
||||
required: false,
|
||||
separate: true,
|
||||
order: [['exerciseNumber', 'ASC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!lesson) {
|
||||
console.log(`❌ Lektion ${lessonId} nicht gefunden`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📚 Lektion: ${lesson.title} (ID: ${lesson.id})`);
|
||||
console.log(` Kurs: ${lesson.course?.title || 'N/A'}`);
|
||||
console.log(` Übungen (via Include): ${lesson.grammarExercises ? lesson.grammarExercises.length : 0}\n`);
|
||||
|
||||
// Prüfe direkt in der Datenbank
|
||||
const directExercises = await VocabGrammarExercise.findAll({
|
||||
where: { lessonId: lesson.id },
|
||||
order: [['exerciseNumber', 'ASC']]
|
||||
});
|
||||
|
||||
console.log(`📊 Direkte Abfrage: ${directExercises.length} Übung(en) gefunden`);
|
||||
directExercises.forEach((ex, idx) => {
|
||||
console.log(` ${idx + 1}. ${ex.title} (ID: ${ex.id}, Typ: ${ex.exerciseTypeId})`);
|
||||
});
|
||||
|
||||
// Plain object
|
||||
const plain = lesson.get({ plain: true });
|
||||
console.log(`\n📦 Plain Object:`);
|
||||
console.log(` grammarExercises: ${plain.grammarExercises ? plain.grammarExercises.length : 'undefined'}`);
|
||||
if (plain.grammarExercises && plain.grammarExercises.length > 0) {
|
||||
plain.grammarExercises.forEach((ex, idx) => {
|
||||
console.log(` ${idx + 1}. ${ex.title} (ID: ${ex.id})`);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler:', error);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
debugLesson();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user