Compare commits
348 Commits
falukant-3
...
Redesign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d44a265ca | ||
|
|
4442937ebd | ||
|
|
59869e077e | ||
|
|
971e09a72a | ||
|
|
bf2b490731 | ||
|
|
fd41a53404 | ||
|
|
a48e907e50 | ||
|
|
a117bad342 | ||
|
|
190cf626f9 | ||
|
|
2bc34acacf | ||
|
|
5f4acbea51 | ||
|
|
6d4ada7b31 | ||
|
|
1bccee3429 | ||
|
|
947d3d0694 | ||
|
|
e76fdbe1ab | ||
|
|
db8be34607 | ||
|
|
407c3b359b | ||
|
|
a2652c983f | ||
|
|
42fe568e2b | ||
|
|
ea7f8d1acc | ||
|
|
af4e5de1ad | ||
|
|
cc80081280 | ||
|
|
444a1b9dcc | ||
|
|
91637ba7a3 | ||
|
|
be7db6ad96 | ||
|
|
a3b550859c | ||
|
|
c58f8c0bf8 | ||
|
|
73304e8af4 | ||
|
|
e21c61b5e3 | ||
|
|
78a44b5189 | ||
|
|
da1d912bdb | ||
|
|
c45a843611 | ||
|
|
b07099b57d | ||
|
|
a7688e4ed5 | ||
|
|
9c91d99bed | ||
|
|
a1ea192a73 | ||
|
|
52fb1ec183 | ||
|
|
b51d396afc | ||
|
|
c8d8254fc1 | ||
|
|
83455f1e83 | ||
|
|
c779be2897 | ||
|
|
8c40144734 | ||
|
|
022cd47e7e | ||
|
|
c7a05c3213 | ||
|
|
a60c6d173c | ||
|
|
971da3e57c | ||
|
|
2f29f43409 | ||
|
|
56c38c04aa | ||
|
|
6b96ee9856 | ||
|
|
23b0c45518 | ||
|
|
10649d9fbf | ||
|
|
6a6cd7b910 | ||
|
|
2958d38c63 | ||
|
|
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';
|
||||
|
||||
@@ -32,6 +36,19 @@ const app = express();
|
||||
// - LOG_ALL_REQ=1: Logge alle Requests
|
||||
const LOG_ALL_REQ = process.env.LOG_ALL_REQ === '1';
|
||||
const LOG_SLOW_REQ_MS = Number.parseInt(process.env.LOG_SLOW_REQ_MS || '500', 10);
|
||||
const defaultCorsOrigins = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:5173',
|
||||
'http://127.0.0.1:3000',
|
||||
'http://127.0.0.1:5173'
|
||||
];
|
||||
const corsOrigins = (process.env.CORS_ORIGINS || process.env.FRONTEND_URL || '')
|
||||
.split(',')
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean);
|
||||
const effectiveCorsOrigins = corsOrigins.length > 0 ? corsOrigins : defaultCorsOrigins;
|
||||
const corsAllowAll = process.env.CORS_ALLOW_ALL === '1';
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const reqId = req.headers['x-request-id'] || (crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(8).toString('hex'));
|
||||
req.reqId = reqId;
|
||||
@@ -47,15 +64,26 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
const corsOptions = {
|
||||
origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
|
||||
origin(origin, callback) {
|
||||
if (!origin) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
if (corsAllowAll || effectiveCorsOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
},
|
||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'userId', 'authCode'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'userid', 'authcode', 'userId', 'authCode'],
|
||||
credentials: true,
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
app.options('*', cors(corsOptions));
|
||||
app.use(express.json()); // To handle JSON request bodies
|
||||
|
||||
app.use('/api/chat', chatRouter);
|
||||
@@ -74,11 +102,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();
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 1235
|
||||
"port": 1236
|
||||
}
|
||||
|
||||
@@ -12,25 +12,51 @@ const productionEnvPath = '/opt/yourpart/backend/.env';
|
||||
const localEnvPath = path.resolve(__dirname, '../.env');
|
||||
|
||||
let envPath = localEnvPath; // Fallback
|
||||
let usingProduction = false;
|
||||
if (fs.existsSync(productionEnvPath)) {
|
||||
envPath = productionEnvPath;
|
||||
console.log('[env] Lade Produktions-.env:', productionEnvPath);
|
||||
// Prüfe Lesbarkeit bevor wir versuchen, sie zu laden
|
||||
try {
|
||||
fs.accessSync(productionEnvPath, fs.constants.R_OK);
|
||||
envPath = productionEnvPath;
|
||||
usingProduction = true;
|
||||
console.log('[env] Produktions-.env gefunden und lesbar:', productionEnvPath);
|
||||
} catch (err) {
|
||||
console.warn('[env] Produktions-.env vorhanden, aber nicht lesbar - verwende lokale .env stattdessen:', productionEnvPath);
|
||||
console.warn('[env] Fehler:', err && err.message);
|
||||
envPath = localEnvPath;
|
||||
}
|
||||
} else {
|
||||
console.log('[env] Lade lokale .env:', localEnvPath);
|
||||
console.log('[env] Produktions-.env nicht gefunden, lade lokale .env:', localEnvPath);
|
||||
}
|
||||
|
||||
// Lade .env-Datei
|
||||
// Lade .env-Datei (robust gegen Fehler)
|
||||
console.log('[env] Versuche .env zu laden von:', envPath);
|
||||
console.log('[env] Datei existiert:', fs.existsSync(envPath));
|
||||
console.log('[env] Datei lesbar:', fs.accessSync ? (() => { try { fs.accessSync(envPath, fs.constants.R_OK); return true; } catch { return false; } })() : 'unbekannt');
|
||||
|
||||
const result = dotenv.config({ path: envPath });
|
||||
if (result.error) {
|
||||
console.warn('[env] Konnte .env nicht laden:', result.error.message);
|
||||
console.warn('[env] Fehler-Details:', result.error);
|
||||
} else {
|
||||
console.log('[env] .env erfolgreich geladen von:', envPath);
|
||||
console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
|
||||
let result;
|
||||
try {
|
||||
result = dotenv.config({ path: envPath });
|
||||
if (result.error) {
|
||||
console.warn('[env] Konnte .env nicht laden:', result.error.message);
|
||||
console.warn('[env] Fehler-Details:', result.error);
|
||||
} else {
|
||||
console.log('[env] .env erfolgreich geladen von:', envPath, usingProduction ? '(production)' : '(local)');
|
||||
console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
|
||||
}
|
||||
} catch (err) {
|
||||
// Sollte nicht passieren, aber falls dotenv intern eine Exception wirft (z.B. EACCES), fange sie ab
|
||||
console.warn('[env] Unerwarteter Fehler beim Laden der .env:', err && err.message);
|
||||
console.warn('[env] Stack:', err && err.stack);
|
||||
if (envPath !== localEnvPath && fs.existsSync(localEnvPath)) {
|
||||
console.log('[env] Versuche stattdessen lokale .env:', localEnvPath);
|
||||
try {
|
||||
result = dotenv.config({ path: localEnvPath });
|
||||
if (!result.error) {
|
||||
console.log('[env] Lokale .env erfolgreich geladen von:', localEnvPath);
|
||||
}
|
||||
} catch (err2) {
|
||||
console.warn('[env] Konnte lokale .env auch nicht laden:', err2 && err2.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Zeige Redis-Konfiguration
|
||||
|
||||
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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,9 @@ class ChatController {
|
||||
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
|
||||
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
|
||||
this.getRoomList = this.getRoomList.bind(this);
|
||||
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
|
||||
this.getOwnRooms = this.getOwnRooms.bind(this);
|
||||
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
|
||||
}
|
||||
|
||||
async getMessages(req, res) {
|
||||
@@ -175,6 +178,41 @@ class ChatController {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getRoomCreateOptions(req, res) {
|
||||
try {
|
||||
const options = await chatService.getRoomCreateOptions();
|
||||
res.status(200).json(options);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getOwnRooms(req, res) {
|
||||
try {
|
||||
const { userid: hashedUserId } = req.headers;
|
||||
const rooms = await chatService.getOwnRooms(hashedUserId);
|
||||
res.status(200).json(rooms);
|
||||
} catch (error) {
|
||||
const status = error.message === 'user_not_found' ? 404 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteOwnRoom(req, res) {
|
||||
try {
|
||||
const { userid: hashedUserId } = req.headers;
|
||||
const roomId = Number.parseInt(req.params.id, 10);
|
||||
if (!Number.isInteger(roomId) || roomId <= 0) {
|
||||
return res.status(400).json({ error: 'invalid_room_id' });
|
||||
}
|
||||
await chatService.deleteOwnRoom(hashedUserId, roomId);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
const status = error.message === 'room_not_found_or_not_owner' || error.message === 'user_not_found' ? 404 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatController;
|
||||
|
||||
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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -26,6 +26,8 @@ class FalukantController {
|
||||
}, { successStatus: 201 });
|
||||
|
||||
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
|
||||
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
|
||||
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
|
||||
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
|
||||
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
|
||||
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
||||
@@ -56,6 +58,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;
|
||||
@@ -92,18 +98,40 @@ class FalukantController {
|
||||
if (!result) throw { status: 404, message: 'No family data found' };
|
||||
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.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||
this.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.executeReputationAction = this._wrapWithUser((userId, req) =>
|
||||
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201 });
|
||||
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 +146,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 +177,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 +204,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 +220,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 +237,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,7 +312,13 @@ class FalukantController {
|
||||
} catch (error) {
|
||||
console.error('Controller error:', error);
|
||||
const status = error.status && typeof error.status === 'number' ? error.status : 500;
|
||||
res.status(status).json({ error: error.message || 'Internal error' });
|
||||
// Wenn error ein Objekt mit status ist, alle Felder außer status übernehmen
|
||||
if (error && typeof error === 'object' && error.status && typeof error.status === 'number') {
|
||||
const { status: errorStatus, ...errorData } = error;
|
||||
res.status(errorStatus).json({ error: error.message || errorData.message || 'Internal error', ...errorData });
|
||||
} else {
|
||||
res.status(status).json({ error: error.message || 'Internal error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
19
backend/migrations/add_chat_room_dialog_fields.sql
Normal file
19
backend/migrations/add_chat_room_dialog_fields.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE chat.room
|
||||
ADD COLUMN IF NOT EXISTS gender_restriction_id INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS min_age INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS max_age INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS password VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS friends_of_owner_only BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS required_user_right_id INTEGER;
|
||||
|
||||
UPDATE chat.room
|
||||
SET friends_of_owner_only = FALSE
|
||||
WHERE friends_of_owner_only IS NULL;
|
||||
|
||||
ALTER TABLE chat.room
|
||||
ALTER COLUMN friends_of_owner_only SET DEFAULT FALSE,
|
||||
ALTER COLUMN friends_of_owner_only SET NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -5,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';
|
||||
@@ -44,6 +45,7 @@ import FalukantStockType from './falukant/type/stock.js';
|
||||
import Knowledge from './falukant/data/product_knowledge.js';
|
||||
import ProductType from './falukant/type/product.js';
|
||||
import TitleOfNobility from './falukant/type/title_of_nobility.js';
|
||||
import TitleBenefit from './falukant/type/title_benefit.js';
|
||||
import TitleRequirement from './falukant/type/title_requirement.js';
|
||||
import Branch from './falukant/data/branch.js';
|
||||
import BranchType from './falukant/type/branch.js';
|
||||
@@ -93,6 +95,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 +108,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 +170,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' });
|
||||
|
||||
@@ -335,6 +353,8 @@ export default function setupAssociations() {
|
||||
|
||||
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
||||
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
|
||||
TitleOfNobility.hasMany(TitleBenefit, { foreignKey: 'titleId', as: 'benefits' });
|
||||
TitleBenefit.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
|
||||
|
||||
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
|
||||
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });
|
||||
@@ -405,6 +425,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 +585,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 +886,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 +1058,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',
|
||||
|
||||
41
backend/models/falukant/type/title_benefit.js
Normal file
41
backend/models/falukant/type/title_benefit.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
/**
|
||||
* Vorteile pro Stand (Adelstitel).
|
||||
* benefit_type: 'tax_share' | 'tax_exempt' | 'office_eligibility' | 'free_party_type' | 'reputation_bonus'
|
||||
* parameters: JSONB, z.B. { officeTypeNames: [] }, { partyTypeIds: [] }, { minPercent: 5, maxPercent: 15 }
|
||||
*/
|
||||
class TitleBenefit extends Model {}
|
||||
|
||||
TitleBenefit.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
titleId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'title_id'
|
||||
},
|
||||
benefitType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
field: 'benefit_type'
|
||||
},
|
||||
parameters: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'TitleBenefit',
|
||||
tableName: 'title_benefit',
|
||||
schema: 'falukant_type',
|
||||
timestamps: false,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
export default TitleBenefit;
|
||||
@@ -4,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';
|
||||
@@ -49,6 +51,7 @@ import ProductType from './falukant/type/product.js';
|
||||
import Knowledge from './falukant/data/product_knowledge.js';
|
||||
import TitleRequirement from './falukant/type/title_requirement.js';
|
||||
import TitleOfNobility from './falukant/type/title_of_nobility.js';
|
||||
import TitleBenefit from './falukant/type/title_benefit.js';
|
||||
import BranchType from './falukant/type/branch.js';
|
||||
import Branch from './falukant/data/branch.js';
|
||||
import Production from './falukant/data/production.js';
|
||||
@@ -87,6 +90,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 +117,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 +140,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,
|
||||
@@ -179,6 +202,7 @@ const models = {
|
||||
ProductType,
|
||||
Knowledge,
|
||||
TitleOfNobility,
|
||||
TitleBenefit,
|
||||
TitleRequirement,
|
||||
BranchType,
|
||||
Branch,
|
||||
@@ -218,6 +242,7 @@ const models = {
|
||||
Credit,
|
||||
DebtorsPrism,
|
||||
HealthActivity,
|
||||
ProductPriceHistory,
|
||||
RegionDistance,
|
||||
VehicleType,
|
||||
Vehicle,
|
||||
@@ -233,6 +258,11 @@ const models = {
|
||||
ElectionResult,
|
||||
PoliticalOfficeHistory,
|
||||
ElectionHistory,
|
||||
RelationshipChangeLog,
|
||||
ChurchOfficeType,
|
||||
ChurchOfficeRequirement,
|
||||
ChurchOffice,
|
||||
ChurchApplication,
|
||||
UndergroundType,
|
||||
Underground,
|
||||
WeatherType,
|
||||
@@ -263,6 +293,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;
|
||||
@@ -15,5 +15,8 @@ router.post('/initOneToOne', authenticate, chatController.initOneToOne);
|
||||
router.post('/oneToOne/sendMessage', authenticate, chatController.sendOneToOneMessage); // Neue Route zum Senden einer Nachricht
|
||||
router.get('/oneToOne/messageHistory', authenticate, chatController.getOneToOneMessageHistory); // Neue Route zum Abrufen der Nachrichtengeschichte
|
||||
router.get('/rooms', chatController.getRoomList);
|
||||
router.get('/room-create-options', authenticate, chatController.getRoomCreateOptions);
|
||||
router.get('/my-rooms', authenticate, chatController.getOwnRooms);
|
||||
router.delete('/my-rooms/:id', authenticate, chatController.deleteOwnRoom);
|
||||
|
||||
export default router;
|
||||
|
||||
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);
|
||||
@@ -37,7 +39,13 @@ router.post('/director/settings', falukantController.setSetting);
|
||||
router.get('/director/:branchId', falukantController.getDirectorForBranch);
|
||||
router.get('/directors', falukantController.getAllDirectors);
|
||||
router.post('/directors', falukantController.updateDirector);
|
||||
|
||||
// Legacy endpoint (wurde in einem Refactor entfernt, wird aber von WidgetTypes/Frontend erwartet)
|
||||
// Liefert eine schlanke, frontend-kompatible Widget-Antwort (ohne hashedIds).
|
||||
router.get('/dashboard-widget', falukantController.getDashboardWidget);
|
||||
|
||||
router.post('/family/acceptmarriageproposal', falukantController.acceptMarriageProposal);
|
||||
router.post('/family/cancel-wooing', falukantController.cancelWooing);
|
||||
router.post('/family/set-heir', falukantController.setHeir);
|
||||
router.get('/heirs/potential', falukantController.getPotentialHeirs);
|
||||
router.post('/heirs/select', falukantController.selectHeir);
|
||||
@@ -46,6 +54,8 @@ 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.post('/reputation/actions', falukantController.executeReputationAction);
|
||||
router.get('/houses/types', falukantController.getHouseTypes);
|
||||
router.get('/houses/buyable', falukantController.getBuyableHouses);
|
||||
router.get('/houses', falukantController.getUserHouse);
|
||||
@@ -55,10 +65,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);
|
||||
@@ -70,13 +83,14 @@ router.get('/health', falukantController.getHealth);
|
||||
router.post('/health', falukantController.healthActivity);
|
||||
router.get('/politics/overview', falukantController.getPoliticsOverview);
|
||||
router.get('/politics/open', falukantController.getOpenPolitics);
|
||||
router.post('/politics/open', falukantController.applyForElections);
|
||||
router.get('/politics/elections', falukantController.getElections);
|
||||
router.post('/politics/elections', falukantController.vote);
|
||||
router.get('/politics/open', falukantController.getOpenPolitics);
|
||||
router.post('/politics/open', falukantController.applyForElections);
|
||||
router.get('/cities', falukantController.getRegions);
|
||||
router.get('/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);
|
||||
});
|
||||
1330
backend/scripts/create-bisaya-course-content.js
Normal file
1330
backend/scripts/create-bisaya-course-content.js
Normal file
File diff suppressed because it is too large
Load Diff
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: 25, targetScore: 80, review: false,
|
||||
cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' },
|
||||
|
||||
{ week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie',
|
||||
desc: 'Balay, Kwarto, Kusina, Pamilya',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2',
|
||||
desc: 'Wohin gehst du? Was machst du heute?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung',
|
||||
desc: 'Asa, dinhi, didto, padulong',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen',
|
||||
desc: 'Ni-kaon ko, Mo-kaon ko - Vergangenheit und Zukunft',
|
||||
targetMin: 25, targetScore: 75, review: true,
|
||||
cultural: 'Cebuano hat keine komplexen Zeiten wie Deutsch. Zeit wird mit Präfixen ausgedrückt.' },
|
||||
|
||||
{ week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum',
|
||||
desc: 'Karon, ugma, gahapon, karon adlaw',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise',
|
||||
desc: 'Tagpila ni? Pwede barato?',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Handeln ist in den Philippinen üblich und erwartet.' },
|
||||
|
||||
{ week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise',
|
||||
desc: '1-100, Preise, Mengen',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der zweiten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 2',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 3: Vertiefung
|
||||
{ week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen',
|
||||
desc: 'Nalipay, nasubo, nahadlok, naguol',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' },
|
||||
|
||||
{ week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen',
|
||||
desc: 'Wörter für verschiedene Gefühle',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden',
|
||||
desc: 'Sakit, maayo, tambal, doktor',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit',
|
||||
desc: 'Wörter rund um den Körper und Gesundheit',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen',
|
||||
desc: 'Palihug, Pwede, Tabang',
|
||||
targetMin: 20, targetScore: 75, review: true,
|
||||
cultural: 'Höflichkeit ist extrem wichtig in der philippinischen Kultur.' },
|
||||
|
||||
{ week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen',
|
||||
desc: 'Wie man höflich fragt und bittet',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie',
|
||||
desc: 'Gespräche mit und über Kinder',
|
||||
targetMin: 15, targetScore: 80, review: false,
|
||||
cultural: 'Kinder sind sehr wichtig in philippinischen Familien.' },
|
||||
|
||||
{ week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel',
|
||||
desc: 'Wörter für Kinder und Spielsachen',
|
||||
targetMin: 20, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung',
|
||||
desc: 'Wiederhole alle Inhalte der dritten Woche',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest',
|
||||
desc: 'Teste dein Wissen aus Woche 3',
|
||||
targetMin: 15, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
// WOCHE 4: Freies Sprechen
|
||||
{ week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1',
|
||||
desc: 'Übe freies Sprechen zu verschiedenen Themen',
|
||||
targetMin: 20, targetScore: 75, review: false,
|
||||
cultural: 'Fehler sind okay! Philippiner schätzen das Bemühen.' },
|
||||
|
||||
{ week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2',
|
||||
desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2',
|
||||
desc: 'Weitere Übung im freien Sprechen',
|
||||
targetMin: 20, targetScore: 75, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3',
|
||||
desc: 'Wiederhole wichtige Vokabeln aus Woche 3',
|
||||
targetMin: 25, targetScore: 85, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche',
|
||||
desc: 'Längere Gespräche zu verschiedenen Themen',
|
||||
targetMin: 25, targetScore: 75, review: false,
|
||||
cultural: 'Je mehr du sprichst, desto besser wirst du!' },
|
||||
|
||||
{ week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung',
|
||||
desc: 'Wiederhole alle wichtigen Inhalte des Kurses',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung',
|
||||
desc: 'Simuliere echte Gesprächssituationen',
|
||||
targetMin: 25, targetScore: 75, review: false,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln',
|
||||
desc: 'Finaler Vokabeltest über den gesamten Kurs',
|
||||
targetMin: 20, targetScore: 80, review: true,
|
||||
cultural: null },
|
||||
|
||||
{ week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung',
|
||||
desc: 'Finale Prüfung über alle Kursinhalte',
|
||||
targetMin: 30, targetScore: 80, review: false,
|
||||
cultural: 'Gratulation zum Abschluss des Kurses!' },
|
||||
|
||||
{ week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks',
|
||||
desc: 'Wichtige kulturelle Hinweise für den Alltag',
|
||||
targetMin: 15, targetScore: 0, review: false,
|
||||
cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' }
|
||||
];
|
||||
|
||||
async function createBisayaCourse(languageId, ownerHashedId) {
|
||||
try {
|
||||
// Finde User
|
||||
const user = await User.findOne({ where: { hashedId: ownerHashedId } });
|
||||
if (!user) {
|
||||
throw new Error(`User mit hashedId ${ownerHashedId} nicht gefunden`);
|
||||
}
|
||||
|
||||
// Prüfe, ob Sprache existiert
|
||||
const [lang] = await sequelize.query(
|
||||
`SELECT id FROM community.vocab_language WHERE id = :langId`,
|
||||
{ replacements: { langId: languageId }, type: sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
if (!lang) {
|
||||
throw new Error(`Sprache mit ID ${languageId} nicht gefunden`);
|
||||
}
|
||||
|
||||
// Erstelle Kurs
|
||||
const shareCode = crypto.randomBytes(8).toString('hex');
|
||||
const course = await VocabCourse.create({
|
||||
ownerUserId: user.id,
|
||||
title: 'Bisaya für Familien - Schnellstart in 4 Wochen',
|
||||
description: 'Lerne Bisaya (Cebuano) schnell und praktisch für den Familienalltag. Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.',
|
||||
languageId: Number(languageId),
|
||||
difficultyLevel: 1,
|
||||
isPublic: true,
|
||||
shareCode
|
||||
});
|
||||
|
||||
console.log(`✅ Kurs erstellt: ${course.id} - "${course.title}"`);
|
||||
console.log(` Share-Code: ${shareCode}`);
|
||||
|
||||
// Erstelle Lektionen
|
||||
for (const lessonData of LESSONS) {
|
||||
const lesson = await VocabCourseLesson.create({
|
||||
courseId: course.id,
|
||||
chapterId: null, // Wird später mit Vokabeln verknüpft
|
||||
lessonNumber: lessonData.num,
|
||||
title: lessonData.title,
|
||||
description: lessonData.desc,
|
||||
weekNumber: lessonData.week,
|
||||
dayNumber: lessonData.day,
|
||||
lessonType: lessonData.type,
|
||||
culturalNotes: lessonData.cultural,
|
||||
targetMinutes: lessonData.targetMin,
|
||||
targetScorePercent: lessonData.targetScore,
|
||||
requiresReview: lessonData.review
|
||||
});
|
||||
console.log(` ✅ Lektion ${lessonData.num}: ${lessonData.title} (Woche ${lessonData.week}, Tag ${lessonData.day})`);
|
||||
}
|
||||
|
||||
console.log(`\n🎉 Kurs erfolgreich erstellt mit ${LESSONS.length} Lektionen!`);
|
||||
console.log(`\n📊 Kurs-Statistik:`);
|
||||
console.log(` - Gesamte Lektionen: ${LESSONS.length}`);
|
||||
console.log(` - Vokabel-Lektionen: ${LESSONS.filter(l => l.type === 'vocab').length}`);
|
||||
console.log(` - Konversations-Lektionen: ${LESSONS.filter(l => l.type === 'conversation').length}`);
|
||||
console.log(` - Grammatik-Lektionen: ${LESSONS.filter(l => l.type === 'grammar').length}`);
|
||||
console.log(` - Wiederholungs-Lektionen: ${LESSONS.filter(l => l.type === 'review').length}`);
|
||||
console.log(` - Durchschnittliche Zeit pro Tag: ~${Math.round(LESSONS.reduce((sum, l) => sum + l.targetMin, 0) / (4 * 5))} Minuten`);
|
||||
console.log(`\n💡 Nächste Schritte:`);
|
||||
console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`);
|
||||
console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`);
|
||||
console.log(` 3. Teile den Kurs mit anderen (Share-Code: ${shareCode})`);
|
||||
|
||||
return course;
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Erstellen des Kurses:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI-Aufruf
|
||||
const languageId = process.argv[2];
|
||||
const ownerHashedId = process.argv[3];
|
||||
|
||||
if (!languageId || !ownerHashedId) {
|
||||
console.error('Verwendung: node create-bisaya-course.js <languageId> <ownerHashedId>');
|
||||
console.error('Beispiel: node create-bisaya-course.js 1 abc123def456');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
createBisayaCourse(languageId, ownerHashedId)
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
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,62 @@
|
||||
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.API_PORT || process.env.PORT || '2020', 10);
|
||||
const API_HOST = process.env.API_HOST || '127.0.0.1';
|
||||
const httpServer = http.createServer(app);
|
||||
// Socket.io wird nur auf HTTPS-Server bereitgestellt, nicht auf HTTP-Server
|
||||
// setupWebSocket(httpServer); // Entfernt: Socket.io nur über HTTPS
|
||||
|
||||
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;
|
||||
const SOCKET_IO_HOST = process.env.SOCKET_IO_HOST || '0.0.0.0';
|
||||
|
||||
if (USE_TLS && TLS_KEY_PATH && TLS_CERT_PATH) {
|
||||
try {
|
||||
httpsServer = https.createServer({
|
||||
key: fs.readFileSync(TLS_KEY_PATH),
|
||||
cert: fs.readFileSync(TLS_CERT_PATH),
|
||||
ca: TLS_CA_PATH ? fs.readFileSync(TLS_CA_PATH) : undefined,
|
||||
}, app);
|
||||
setupWebSocket(httpsServer);
|
||||
console.log(`[Socket.io] HTTPS-Server für Socket.io konfiguriert auf Port ${SOCKET_IO_PORT}`);
|
||||
} catch (err) {
|
||||
console.error('[Socket.io] Fehler beim Laden der TLS-Zertifikate:', err.message);
|
||||
console.error('[Socket.io] Socket.io wird nicht verfügbar sein');
|
||||
}
|
||||
} else {
|
||||
console.warn('[Socket.io] TLS nicht konfiguriert - Socket.io wird nicht verfügbar sein');
|
||||
}
|
||||
|
||||
syncDatabase().then(() => {
|
||||
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, API_HOST, () => {
|
||||
console.log(`[API] HTTP-Server läuft auf ${API_HOST}:${API_PORT}`);
|
||||
});
|
||||
|
||||
// Socket.io-Server auf Port 4443 (extern, direkt erreichbar)
|
||||
if (httpsServer) {
|
||||
httpsServer.listen(SOCKET_IO_PORT, SOCKET_IO_HOST, () => {
|
||||
console.log(`[Socket.io] HTTPS-Server läuft auf ${SOCKET_IO_HOST}:${SOCKET_IO_PORT}`);
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
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();
|
||||
@@ -1,7 +1,9 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import amqp from 'amqplib/callback_api.js';
|
||||
import User from '../models/community/user.js';
|
||||
import Room from '../models/chat/room.js';
|
||||
|
||||
const RABBITMQ_URL = 'amqp://localhost';
|
||||
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost';
|
||||
const QUEUE = 'oneToOne_messages';
|
||||
|
||||
class ChatService {
|
||||
@@ -11,11 +13,37 @@ class ChatService {
|
||||
this.users = [];
|
||||
this.randomChats = [];
|
||||
this.oneToOneChats = [];
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
this.initRabbitMq();
|
||||
}
|
||||
|
||||
initRabbitMq() {
|
||||
amqp.connect(RABBITMQ_URL, (err, connection) => {
|
||||
if (err) throw err;
|
||||
connection.createChannel((err, channel) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
console.warn(`[chatService] RabbitMQ nicht erreichbar (${RABBITMQ_URL}) - fallback ohne Queue wird verwendet.`);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.on('error', (connectionError) => {
|
||||
console.warn('[chatService] RabbitMQ-Verbindung fehlerhaft:', connectionError.message);
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
console.warn('[chatService] RabbitMQ-Verbindung geschlossen.');
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
});
|
||||
|
||||
connection.createChannel((channelError, channel) => {
|
||||
if (channelError) {
|
||||
console.warn('[chatService] RabbitMQ-Channel konnte nicht erstellt werden:', channelError.message);
|
||||
return;
|
||||
}
|
||||
this.channel = channel;
|
||||
this.amqpAvailable = true;
|
||||
channel.assertQueue(QUEUE, { durable: false });
|
||||
});
|
||||
});
|
||||
@@ -116,8 +144,14 @@ class ChatService {
|
||||
history: [messageBundle],
|
||||
});
|
||||
}
|
||||
if (this.channel) {
|
||||
this.channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(messageBundle)));
|
||||
if (this.channel && this.amqpAvailable) {
|
||||
try {
|
||||
this.channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(messageBundle)));
|
||||
} catch (error) {
|
||||
console.warn('[chatService] sendToQueue fehlgeschlagen, Queue-Bridge vorübergehend deaktiviert:', error.message);
|
||||
this.channel = null;
|
||||
this.amqpAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +182,66 @@ class ChatService {
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async getRoomCreateOptions() {
|
||||
const { default: UserRightType } = await import('../models/type/user_right.js');
|
||||
const { default: InterestType } = await import('../models/type/interest.js');
|
||||
|
||||
const [rights, interests] = await Promise.all([
|
||||
UserRightType.findAll({
|
||||
attributes: ['id', 'title'],
|
||||
order: [['id', 'ASC']]
|
||||
}),
|
||||
InterestType.findAll({
|
||||
attributes: ['id', 'name'],
|
||||
order: [['id', 'ASC']]
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
rights: rights.map((r) => ({ id: r.id, title: r.title })),
|
||||
roomTypes: interests.map((i) => ({ id: i.id, name: i.name }))
|
||||
};
|
||||
}
|
||||
|
||||
async getOwnRooms(hashedUserId) {
|
||||
const user = await User.findOne({
|
||||
where: { hashedId: hashedUserId },
|
||||
attributes: ['id']
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('user_not_found');
|
||||
}
|
||||
|
||||
return Room.findAll({
|
||||
where: { ownerId: user.id },
|
||||
attributes: ['id', 'title', 'isPublic', 'roomTypeId', 'ownerId'],
|
||||
order: [['title', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
async deleteOwnRoom(hashedUserId, roomId) {
|
||||
const user = await User.findOne({
|
||||
where: { hashedId: hashedUserId },
|
||||
attributes: ['id']
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('user_not_found');
|
||||
}
|
||||
|
||||
const deleted = await Room.destroy({
|
||||
where: {
|
||||
id: roomId,
|
||||
ownerId: user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!deleted) {
|
||||
throw new Error('room_not_found_or_not_owner');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChatService();
|
||||
|
||||
@@ -2,7 +2,10 @@ import net from 'net';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DEFAULT_CONFIG = { host: 'localhost', port: 1235 };
|
||||
const DEFAULT_CONFIG = {
|
||||
host: process.env.CHAT_TCP_HOST || 'localhost',
|
||||
port: Number.parseInt(process.env.CHAT_TCP_PORT || '1235', 10),
|
||||
};
|
||||
|
||||
function loadBridgeConfig() {
|
||||
try {
|
||||
|
||||
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
@@ -2,38 +2,110 @@
|
||||
import { Server } from 'socket.io';
|
||||
import amqp from 'amqplib/callback_api.js';
|
||||
|
||||
const RABBITMQ_URL = 'amqp://localhost';
|
||||
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost';
|
||||
const QUEUE = 'chat_messages';
|
||||
const MAX_PENDING_MESSAGES = 500;
|
||||
|
||||
function routeMessage(io, message) {
|
||||
if (!message || typeof message !== 'object') return;
|
||||
|
||||
if (message.socketId) {
|
||||
io.to(message.socketId).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
if (message.recipientSocketId) {
|
||||
io.to(message.recipientSocketId).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
if (message.roomId) {
|
||||
io.to(String(message.roomId)).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
if (message.room) {
|
||||
io.to(String(message.room)).emit('newMessage', message);
|
||||
return;
|
||||
}
|
||||
|
||||
io.emit('newMessage', message);
|
||||
}
|
||||
|
||||
export function setupWebSocket(server) {
|
||||
const io = new Server(server);
|
||||
let channel = null;
|
||||
let pendingMessages = [];
|
||||
|
||||
const flushPendingMessages = () => {
|
||||
if (!channel || pendingMessages.length === 0) return;
|
||||
const queued = pendingMessages;
|
||||
pendingMessages = [];
|
||||
for (const message of queued) {
|
||||
try {
|
||||
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
|
||||
} catch (err) {
|
||||
console.warn('[webSocketService] Flush fehlgeschlagen, Nachricht bleibt im Fallback:', err.message);
|
||||
pendingMessages.unshift(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
amqp.connect(RABBITMQ_URL, (err, connection) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
console.warn(`[webSocketService] RabbitMQ nicht erreichbar (${RABBITMQ_URL}) - WebSocket läuft ohne Queue-Bridge.`);
|
||||
return;
|
||||
}
|
||||
|
||||
connection.createChannel((err, channel) => {
|
||||
if (err) throw err;
|
||||
connection.on('error', (connectionError) => {
|
||||
console.warn('[webSocketService] RabbitMQ-Verbindung fehlerhaft:', connectionError.message);
|
||||
channel = null;
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
console.warn('[webSocketService] RabbitMQ-Verbindung geschlossen.');
|
||||
channel = null;
|
||||
});
|
||||
|
||||
connection.createChannel((channelError, createdChannel) => {
|
||||
if (channelError) {
|
||||
console.warn('[webSocketService] RabbitMQ-Channel konnte nicht erstellt werden:', channelError.message);
|
||||
return;
|
||||
}
|
||||
|
||||
channel = createdChannel;
|
||||
channel.assertQueue(QUEUE, { durable: false });
|
||||
channel.consume(QUEUE, (msg) => {
|
||||
if (!msg) return;
|
||||
const message = JSON.parse(msg.content.toString());
|
||||
routeMessage(io, message);
|
||||
}, { noAck: true });
|
||||
flushPendingMessages();
|
||||
});
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Client connected via WebSocket');
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Client connected via WebSocket');
|
||||
|
||||
// Konsumiert Nachrichten aus RabbitMQ und sendet sie an den WebSocket-Client
|
||||
channel.consume(QUEUE, (msg) => {
|
||||
const message = JSON.parse(msg.content.toString());
|
||||
io.emit('newMessage', message); // Broadcast an alle Clients
|
||||
}, { noAck: true });
|
||||
|
||||
// Empfangt eine Nachricht vom WebSocket-Client und sendet sie an die RabbitMQ-Warteschlange
|
||||
socket.on('newMessage', (message) => {
|
||||
socket.on('newMessage', (message) => {
|
||||
if (channel) {
|
||||
try {
|
||||
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[webSocketService] sendToQueue fehlgeschlagen, nutze In-Memory-Fallback:', err.message);
|
||||
channel = null;
|
||||
}
|
||||
}
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected');
|
||||
});
|
||||
});
|
||||
if (!channel) {
|
||||
pendingMessages.push(message);
|
||||
if (pendingMessages.length > MAX_PENDING_MESSAGES) {
|
||||
pendingMessages = pendingMessages.slice(-MAX_PENDING_MESSAGES);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Client disconnected');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
37
backend/tools/dumpGiftsDebug.js
Normal file
37
backend/tools/dumpGiftsDebug.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import PromotionalGift from '../models/falukant/type/promotional_gift.js';
|
||||
import PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
|
||||
import PromotionalGiftCharacterTrait from '../models/falukant/predefine/promotional_gift_character_trait.js';
|
||||
|
||||
async function dump() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('DB connected');
|
||||
|
||||
const gifts = await PromotionalGift.findAll({
|
||||
include: [
|
||||
{ model: PromotionalGiftMood, as: 'promotionalgiftmoods', attributes: ['moodId', 'suitability'], required: false },
|
||||
{ model: PromotionalGiftCharacterTrait, as: 'characterTraits', attributes: ['traitId', 'suitability'], required: false }
|
||||
]
|
||||
});
|
||||
|
||||
console.log(`found ${gifts.length} gifts`);
|
||||
for (const g of gifts) {
|
||||
console.log('---');
|
||||
console.log('id:', g.id, 'name:', g.name, 'raw value type:', typeof g.value, 'value:', g.value);
|
||||
try {
|
||||
const plain = g.get({ plain: true });
|
||||
console.log('plain value:', JSON.stringify(plain));
|
||||
} catch (e) {
|
||||
console.log('could not stringify plain', e);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('dump failed', err);
|
||||
process.exit(2);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
dump();
|
||||
61
backend/tools/repairPromotionalGifts.js
Normal file
61
backend/tools/repairPromotionalGifts.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import PromotionalGift from '../models/falukant/type/promotional_gift.js';
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
|
||||
// Mapping basierend auf initializeFalukantTypes.js
|
||||
const seedValues = {
|
||||
'Gold Coin': 100,
|
||||
'Silk Scarf': 50,
|
||||
'Exotic Perfume': 200,
|
||||
'Crystal Pendant': 150,
|
||||
'Leather Journal': 75,
|
||||
'Fine Wine': 120,
|
||||
'Artisan Chocolate': 40,
|
||||
'Pearl Necklace': 300,
|
||||
'Rare Painting': 500,
|
||||
'Silver Watch': 250,
|
||||
'Cat': 70,
|
||||
'Dog': 150,
|
||||
'Horse': 1000
|
||||
};
|
||||
|
||||
async function repair() {
|
||||
console.log('Repair promotional_gift values - starting');
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('DB connection ok');
|
||||
|
||||
// Liste aller problematischen Einträge
|
||||
const [rows] = await sequelize.query("SELECT id, name, value FROM falukant_type.promotional_gift WHERE value IS NULL OR value <= 0");
|
||||
if (!rows.length) {
|
||||
console.log('No invalid promotional_gift rows found. Nothing to do.');
|
||||
return process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${rows.length} invalid promotional_gift rows:`);
|
||||
for (const r of rows) console.log(` id=${r.id} name='${r.name}' value=${r.value}`);
|
||||
|
||||
// Update rows where we have a seed mapping
|
||||
let updated = 0;
|
||||
for (const r of rows) {
|
||||
const seed = seedValues[r.name];
|
||||
if (seed && Number(seed) > 0) {
|
||||
await PromotionalGift.update({ value: seed }, { where: { id: r.id } });
|
||||
console.log(` updated id=${r.id} name='${r.name}' -> value=${seed}`);
|
||||
updated++;
|
||||
} else {
|
||||
console.warn(` no seed value for id=${r.id} name='${r.name}' - skipping`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Done. Updated ${updated} rows. Remaining invalid: `);
|
||||
const [left] = await sequelize.query("SELECT id, name, value FROM falukant_type.promotional_gift WHERE value IS NULL OR value <= 0");
|
||||
for (const l of left) console.log(` id=${l.id} name='${l.name}' value=${l.value}`);
|
||||
console.log('If any remain, inspect and adjust manually.');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Repair failed:', err);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
repair();
|
||||
20
backend/tools/simulate_getGiftCost.js
Normal file
20
backend/tools/simulate_getGiftCost.js
Normal file
@@ -0,0 +1,20 @@
|
||||
function getGiftCostLocal(value, titleOfNobility, lowestTitleOfNobility) {
|
||||
const val = Number(value) || 0;
|
||||
const title = Number(titleOfNobility) || 1;
|
||||
const lowest = Number(lowestTitleOfNobility) || 1;
|
||||
const titleLevel = title - lowest + 1;
|
||||
const cost = Math.round(val * Math.pow(1 + titleLevel * 0.3, 1.3) * 100) / 100;
|
||||
return Number.isFinite(cost) ? cost : 0;
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{ giftValue: 100, title: 3, lowest: 1 },
|
||||
{ giftValue: '200', title: '2', lowest: '1' },
|
||||
{ giftValue: null, title: null, lowest: null },
|
||||
{ giftValue: undefined, title: undefined, lowest: undefined },
|
||||
{ giftValue: 'abc', title: 5, lowest: 1 }
|
||||
];
|
||||
|
||||
for (const c of cases) {
|
||||
console.log(`in=${JSON.stringify(c)} -> cost=${getGiftCostLocal(c.giftValue, c.title, c.lowest)}`);
|
||||
}
|
||||
38
backend/tools/testGiftCost.js
Normal file
38
backend/tools/testGiftCost.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Kleine Testhilfe: extrahiere getGiftCost aus service-file via eval (schneller Smoke-test ohne DB)
|
||||
const svcPath = path.resolve(process.cwd(), 'services', 'falukantService.js');
|
||||
const src = readFileSync(svcPath, 'utf8');
|
||||
|
||||
// Extrahiere die getGiftCost-Funktion via Regex (vereinfachte Annahme)
|
||||
const re = /async getGiftCost\([\s\S]*?\n\s*}\n/;
|
||||
const match = src.match(re);
|
||||
if (!match) {
|
||||
console.error('getGiftCost function not found');
|
||||
process.exit(2);
|
||||
}
|
||||
const funcSrc = match[0];
|
||||
// Wrappe in Async-Function und erzeuge getGiftCost im lokalen Scope
|
||||
const wrapper = `(async () => { ${funcSrc}; return getGiftCost; })()`;
|
||||
// eslint-disable-next-line no-eval
|
||||
const getGiftCostPromise = eval(wrapper);
|
||||
let getGiftCost;
|
||||
getGiftCostPromise.then(f => { getGiftCost = f; runTests(); }).catch(e => { console.error('eval failed', e); process.exit(2); });
|
||||
|
||||
function runTests() {
|
||||
const cases = [
|
||||
{ value: 100, title: 3, lowest: 1 },
|
||||
{ value: '200', title: '2', lowest: '1' },
|
||||
{ value: null, title: null, lowest: null },
|
||||
{ value: 'abc', title: 5, lowest: 1 }
|
||||
];
|
||||
|
||||
for (const c of cases) {
|
||||
getGiftCost(c.value, c.title, c.lowest).then(out => {
|
||||
console.log(`in=${JSON.stringify(c)} -> cost=${out}`);
|
||||
}).catch(err => console.error('error calling getGiftCost', err));
|
||||
}
|
||||
}
|
||||
// Ende Patch
|
||||
@@ -282,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,
|
||||
|
||||
@@ -9,6 +9,7 @@ import PromotionalGiftMood from "../../models/falukant/predefine/promotional_gif
|
||||
import { sequelize } from '../sequelize.js';
|
||||
import HouseType from '../../models/falukant/type/house.js';
|
||||
import TitleOfNobility from "../../models/falukant/type/title_of_nobility.js";
|
||||
import TitleBenefit from "../../models/falukant/type/title_benefit.js";
|
||||
import PartyType from "../../models/falukant/type/party.js";
|
||||
import MusicType from "../../models/falukant/type/music.js";
|
||||
import BanquetteType from "../../models/falukant/type/banquette.js";
|
||||
@@ -16,6 +17,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";
|
||||
@@ -37,6 +40,7 @@ export const initializeFalukantTypes = async () => {
|
||||
|
||||
// Adelstitel VOR Haustypen initialisieren
|
||||
await initializeFalukantTitlesOfNobility();
|
||||
await initializeTitleBenefits();
|
||||
|
||||
await initializeFalukantHouseTypes();
|
||||
await initializeFalukantPartyTypes();
|
||||
@@ -47,6 +51,8 @@ export const initializeFalukantTypes = async () => {
|
||||
await initializePoliticalOfficeBenefitTypes();
|
||||
await initializePoliticalOfficeTypes();
|
||||
await initializePoliticalOfficePrerequisites();
|
||||
await initializeChurchOfficeTypes();
|
||||
await initializeChurchOfficePrerequisites();
|
||||
await initializeUndergroundTypes();
|
||||
await initializeVehicleTypes();
|
||||
await initializeFalukantWeatherTypes();
|
||||
@@ -1024,6 +1030,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({
|
||||
@@ -1070,6 +1206,65 @@ export const initializeFalukantTitlesOfNobility = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Standesvorteile: tax_share, tax_exempt, office_eligibility, free_party_type, reputation_bonus */
|
||||
async function initializeTitleBenefits() {
|
||||
const titles = await TitleOfNobility.findAll({ attributes: ['id', 'labelTr', 'level'] });
|
||||
const byLabel = new Map(titles.map(t => [t.labelTr, t]));
|
||||
const benefits = [];
|
||||
// tax_share: oberster Stand einer Region bekommt Steuer – Titel mit hohem level (z.B. ab count)
|
||||
const taxShareTitles = ['count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'];
|
||||
for (const label of taxShareTitles) {
|
||||
const t = byLabel.get(label);
|
||||
if (t) benefits.push({ titleId: t.id, benefitType: 'tax_share', parameters: {} });
|
||||
}
|
||||
// tax_exempt: z.B. noncivil, oder hohe Titel
|
||||
const taxExemptTitles = ['noncivil', 'king', 'prince-regent'];
|
||||
for (const label of taxExemptTitles) {
|
||||
const t = byLabel.get(label);
|
||||
if (t) benefits.push({ titleId: t.id, benefitType: 'tax_exempt', parameters: {} });
|
||||
}
|
||||
// office_eligibility: pro Titel eine Zeile mit allen erlaubten Ämtern (officeTypeNames)
|
||||
const officeEligibility = [
|
||||
{ label: 'assessor', titles: ['civil', 'sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
|
||||
{ label: 'council', titles: ['sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
|
||||
{ label: 'taxman', titles: ['townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'] },
|
||||
{ label: 'chancellor', titles: ['king'] }
|
||||
];
|
||||
const titleToOffices = new Map();
|
||||
for (const { label: officeName, titles: allowedTitles } of officeEligibility) {
|
||||
for (const label of allowedTitles) {
|
||||
const t = byLabel.get(label);
|
||||
if (t) {
|
||||
const list = titleToOffices.get(t.id) || [];
|
||||
if (!list.includes(officeName)) list.push(officeName);
|
||||
titleToOffices.set(t.id, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [titleId, officeTypeNames] of titleToOffices) {
|
||||
benefits.push({ titleId, benefitType: 'office_eligibility', parameters: { officeTypeNames } });
|
||||
}
|
||||
// free_party_type: z.B. "wedding" für civil+ (partyTypeIds oder labelTr)
|
||||
const freePartyTitles = ['sir', 'townlord', 'by', 'landlord', 'knight', 'baron', 'count', 'palsgrave', 'margrave', 'landgrave', 'ruler', 'elector', 'imperial-prince', 'duke', 'grand-duke', 'prince-regent', 'king'];
|
||||
for (const label of freePartyTitles) {
|
||||
const t = byLabel.get(label);
|
||||
if (t) benefits.push({ titleId: t.id, benefitType: 'free_party_type', parameters: { partyTypeLabelTrs: ['wedding'] } });
|
||||
}
|
||||
// reputation_bonus: zufällig 5–15 % für ausgewählte Stände (hier: knight, count, duke)
|
||||
const reputationBonusTitles = ['knight', 'count', 'duke'];
|
||||
for (const label of reputationBonusTitles) {
|
||||
const t = byLabel.get(label);
|
||||
if (t) benefits.push({ titleId: t.id, benefitType: 'reputation_bonus', parameters: { minPercent: 5, maxPercent: 15 } });
|
||||
}
|
||||
for (const b of benefits) {
|
||||
await TitleBenefit.findOrCreate({
|
||||
where: { titleId: b.titleId, benefitType: b.benefitType },
|
||||
defaults: { parameters: b.parameters }
|
||||
});
|
||||
}
|
||||
console.log(`[Falukant] Standesvorteile (title_benefit) initialisiert: ${benefits.length} Einträge`);
|
||||
}
|
||||
|
||||
const weatherTypes = [
|
||||
{ tr: "sunny" },
|
||||
{ tr: "cloudy" },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user