Compare commits
419 Commits
d08022ab94
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de52b6f26d | ||
|
|
43dd1a3b7f | ||
|
|
22f1803e7d | ||
|
|
42e894d4e4 | ||
|
|
9b88a98a20 | ||
|
|
f2343098d2 | ||
|
|
57ab85fe10 | ||
|
|
ce36315b58 | ||
|
|
80d8caee88 | ||
|
|
b3607849d2 | ||
|
|
d901257be1 | ||
|
|
d7c59df225 | ||
|
|
f7e0d97174 | ||
|
|
2055c11fd9 | ||
|
|
f98352088e | ||
|
|
63d9aab66a | ||
|
|
5f9e0a5a49 | ||
|
|
9af974d2f2 | ||
|
|
c0f9fc8970 | ||
|
|
876ee2ab49 | ||
|
|
2977b152a2 | ||
|
|
c7d33525ff | ||
|
|
1774d7df88 | ||
|
|
2c58ef37c4 | ||
|
|
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 | ||
|
|
3722bcf8c8 | ||
|
|
0372d213c0 | ||
|
|
c322eb1e5a | ||
|
|
b34dcac685 | ||
|
|
4850f50c66 | ||
|
|
5996f819e8 | ||
|
|
4d967fe7a2 | ||
|
|
bb91c2bbe5 | ||
|
|
511df52c3c | ||
|
|
d42e1da14b | ||
|
|
75dbd78da1 | ||
|
|
c90b7785c0 | ||
|
|
c17af04cbf | ||
|
|
f5e3a9a4a2 | ||
|
|
dab3391aa2 | ||
|
|
0336c55560 | ||
|
|
8e618ab443 | ||
|
|
352d672bdd | ||
|
|
df64c0a4b5 | ||
|
|
83597d9e02 | ||
|
|
a09220b881 | ||
|
|
5623f3af09 | ||
|
|
820b5e8570 | ||
|
|
dc72ed2feb | ||
|
|
ea468c9878 | ||
|
|
d1b683344e | ||
|
|
a82ec7de3f | ||
|
|
560a9efc69 | ||
|
|
4f8b1e33fa | ||
|
|
38dd51f757 | ||
|
|
38f23cc6ae | ||
|
|
6cf8fa8a9c | ||
|
|
f9ea4715d7 | ||
|
|
b34b374f76 | ||
|
|
83d1168f26 | ||
|
|
91009f52cd | ||
|
|
c6dfca7052 | ||
|
|
aaeaeeed24 | ||
|
|
c5804f764c | ||
|
|
fbe0d1bcd1 | ||
|
|
2fb440f033 | ||
|
|
a8a136a9ce | ||
|
|
fcbb903839 | ||
|
|
ac45a2ba26 | ||
|
|
afe15dd4f5 | ||
|
|
e3df88bea0 | ||
|
|
c69a414f78 |
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>
|
||||
230
PERFORMANCE_ANALYSIS.md
Normal file
230
PERFORMANCE_ANALYSIS.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Backend Performance-Analyse: Sell-Funktionen
|
||||
|
||||
## Identifizierte Performance-Probleme
|
||||
|
||||
### 1. **N+1 Query Problem in `sellAllProducts()`**
|
||||
|
||||
**Problem:**
|
||||
Die Funktion `sellAllProducts()` macht für jedes Inventory-Item mehrere separate Datenbankabfragen:
|
||||
|
||||
1. **Erste Schleife (Zeile 1702-1711):**
|
||||
- `calcRegionalSellPrice()` → macht `TownProductWorth.findOne()` für jedes Item
|
||||
- `getCumulativeTaxPercentWithExemptions()` → macht mehrere Queries pro Item:
|
||||
- `FalukantCharacter.findOne()`
|
||||
- `PoliticalOffice.findAll()` mit Includes
|
||||
- Rekursive SQL-Query für Steuerberechnung
|
||||
- `addSellItem()` → macht `Branch.findOne()` und `DaySell.findOne()`/`create()` für jedes Item
|
||||
|
||||
2. **Zweite Schleife (Zeile 1714-1724):**
|
||||
- `RegionData.findOne()` für jedes Item
|
||||
- `getCumulativeTaxPercent()` → rekursive SQL-Query für jedes Item
|
||||
- `calcRegionalSellPrice()` → erneut `TownProductWorth.findOne()` für jedes Item
|
||||
|
||||
**Beispiel:** Bei 10 Items werden gemacht:
|
||||
- 10x `TownProductWorth.findOne()` (2x pro Item)
|
||||
- 10x `RegionData.findOne()`
|
||||
- 10x `getCumulativeTaxPercentWithExemptions()` (mit mehreren Queries)
|
||||
- 10x `getCumulativeTaxPercent()` (rekursive SQL)
|
||||
- 10x `addSellItem()` (mit 2 Queries pro Item)
|
||||
- = **~70+ Datenbankabfragen für 10 Items**
|
||||
|
||||
### 2. **Ineffiziente `addSellItem()` Implementierung**
|
||||
|
||||
**Problem:**
|
||||
- Wird für jedes Item einzeln aufgerufen
|
||||
- Macht `Branch.findOne()` für jedes Item (könnte gecacht werden)
|
||||
- `DaySell.findOne()` und `create()`/`update()` für jedes Item
|
||||
|
||||
**Lösung:** Batch-Operation implementieren, die alle DaySell Einträge auf einmal verarbeitet.
|
||||
|
||||
### 3. **Doppelte Berechnungen in `sellAllProducts()`**
|
||||
|
||||
**Problem:**
|
||||
- Preis wird zweimal berechnet (Zeile 1705 und 1718)
|
||||
- Steuer wird zweimal berechnet (Zeile 1706 und 1717)
|
||||
- `calcRegionalSellPrice()` wird zweimal aufgerufen mit denselben Parametern
|
||||
|
||||
### 4. **Fehlende Indizes**
|
||||
|
||||
**Potenzielle fehlende Indizes:**
|
||||
- `falukant_data.town_product_worth(product_id, region_id)` - sollte unique sein
|
||||
- `falukant_data.inventory(stock_id, product_id, quality)` - für schnelle Lookups
|
||||
- `falukant_data.knowledge(character_id, product_id)` - für Knowledge-Lookups
|
||||
- `falukant_data.political_office(character_id)` - für Steuerbefreiungen
|
||||
|
||||
### 5. **Ineffiziente `getCumulativeTaxPercentWithExemptions()`**
|
||||
|
||||
**Problem:**
|
||||
- Lädt alle PoliticalOffices jedes Mal neu, auch wenn sich nichts geändert hat
|
||||
- Macht komplexe rekursive SQL-Query für jedes Item separat
|
||||
- Könnte gecacht werden (z.B. pro User+Region Kombination)
|
||||
|
||||
## Empfohlene Optimierungen
|
||||
|
||||
### 1. **Batch-Loading für `sellAllProducts()`**
|
||||
|
||||
```javascript
|
||||
async sellAllProducts(hashedUserId, branchId) {
|
||||
// ... existing code ...
|
||||
|
||||
// Batch-Load alle benötigten Daten VOR den Schleifen
|
||||
const regionIds = [...new Set(inventory.map(item => item.stock.branch.regionId))];
|
||||
const productIds = [...new Set(inventory.map(item => item.productType.id))];
|
||||
|
||||
// 1. Lade alle TownProductWorth Einträge auf einmal
|
||||
const townWorths = await TownProductWorth.findAll({
|
||||
where: {
|
||||
productId: { [Op.in]: productIds },
|
||||
regionId: { [Op.in]: regionIds }
|
||||
}
|
||||
});
|
||||
const worthMap = new Map();
|
||||
townWorths.forEach(tw => {
|
||||
worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent);
|
||||
});
|
||||
|
||||
// 2. Lade alle RegionData auf einmal
|
||||
const regions = await RegionData.findAll({
|
||||
where: { id: { [Op.in]: regionIds } }
|
||||
});
|
||||
const regionMap = new Map(regions.map(r => [r.id, r]));
|
||||
|
||||
// 3. Berechne Steuern für alle Regionen auf einmal
|
||||
const taxMap = new Map();
|
||||
for (const regionId of regionIds) {
|
||||
const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId);
|
||||
taxMap.set(regionId, tax);
|
||||
}
|
||||
|
||||
// 4. Berechne Preise und Steuern in einer Schleife
|
||||
const sellItems = [];
|
||||
for (const item of inventory) {
|
||||
const regionId = item.stock.branch.regionId;
|
||||
const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50;
|
||||
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
|
||||
const pricePerUnit = calcRegionalSellPrice(item.productType, knowledgeVal, regionId, worthPercent);
|
||||
const cumulativeTax = taxMap.get(regionId);
|
||||
// ... rest of calculation ...
|
||||
sellItems.push({ branchId: item.stock.branch.id, productId: item.productType.id, quantity: item.quantity });
|
||||
}
|
||||
|
||||
// 5. Batch-Update DaySell Einträge
|
||||
await this.addSellItemsBatch(sellItems);
|
||||
|
||||
// ... rest of code ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Batch-Operation für `addSellItem()`**
|
||||
|
||||
```javascript
|
||||
async addSellItemsBatch(sellItems) {
|
||||
// Gruppiere nach (regionId, productId, sellerId)
|
||||
const grouped = new Map();
|
||||
for (const item of sellItems) {
|
||||
const branch = await Branch.findByPk(item.branchId);
|
||||
if (!branch) continue;
|
||||
|
||||
const key = `${branch.regionId}-${item.productId}-${item.sellerId}`;
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, {
|
||||
regionId: branch.regionId,
|
||||
productId: item.productId,
|
||||
sellerId: item.sellerId,
|
||||
quantity: 0
|
||||
});
|
||||
}
|
||||
grouped.get(key).quantity += item.quantity;
|
||||
}
|
||||
|
||||
// Batch-Update oder Create
|
||||
for (const [key, data] of grouped) {
|
||||
const [daySell, created] = await DaySell.findOrCreate({
|
||||
where: {
|
||||
regionId: data.regionId,
|
||||
productId: data.productId,
|
||||
sellerId: data.sellerId
|
||||
},
|
||||
defaults: { quantity: data.quantity }
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
daySell.quantity += data.quantity;
|
||||
await daySell.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Caching für `getCumulativeTaxPercentWithExemptions()`**
|
||||
|
||||
```javascript
|
||||
// Cache für Steuerberechnungen (z.B. 5 Minuten)
|
||||
const taxCache = new Map();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten
|
||||
|
||||
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
|
||||
const cacheKey = `${userId}-${regionId}`;
|
||||
const cached = taxCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
// ... existing calculation ...
|
||||
|
||||
taxCache.set(cacheKey, { value: tax, timestamp: Date.now() });
|
||||
return tax;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Optimierte `calcRegionalSellPrice()`**
|
||||
|
||||
```javascript
|
||||
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
||||
// Wenn worthPercent nicht übergeben wurde UND wir es nicht aus dem Cache haben,
|
||||
// dann hole es aus der DB
|
||||
if (worthPercent === null) {
|
||||
const townWorth = await TownProductWorth.findOne({
|
||||
where: { productId: product.id, regionId: regionId }
|
||||
});
|
||||
worthPercent = townWorth?.worthPercent || 50;
|
||||
}
|
||||
|
||||
// ... rest of calculation ...
|
||||
}
|
||||
```
|
||||
|
||||
### 5. **Datenbank-Indizes hinzufügen**
|
||||
|
||||
```sql
|
||||
-- Index für town_product_worth (sollte unique sein)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_town_product_worth_product_region
|
||||
ON falukant_data.town_product_worth(product_id, region_id);
|
||||
|
||||
-- Index für inventory Lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_stock_product_quality
|
||||
ON falukant_data.inventory(stock_id, product_id, quality);
|
||||
|
||||
-- Index für knowledge Lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_character_product
|
||||
ON falukant_data.knowledge(character_id, product_id);
|
||||
|
||||
-- Index für political_office Lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_political_office_character
|
||||
ON falukant_data.political_office(character_id);
|
||||
```
|
||||
|
||||
## Geschätzter Performance-Gewinn
|
||||
|
||||
- **Vorher:** ~70+ Queries für 10 Items
|
||||
- **Nachher:** ~15-20 Queries für 10 Items (Batch-Loading + Caching)
|
||||
- **Geschätzte Verbesserung:** 70-80% weniger Datenbankabfragen
|
||||
|
||||
## Priorität
|
||||
|
||||
1. **Hoch:** Batch-Loading für `sellAllProducts()` (größter Impact)
|
||||
2. **Hoch:** Batch-Operation für `addSellItem()`
|
||||
3. **Mittel:** Caching für Steuerberechnungen
|
||||
4. **Mittel:** Datenbank-Indizes
|
||||
5. **Niedrig:** Doppelte Berechnungen entfernen
|
||||
|
||||
601
SELL_OVERVIEW.md
Normal file
601
SELL_OVERVIEW.md
Normal file
@@ -0,0 +1,601 @@
|
||||
# Übersicht: Sell-Funktionen und verwendete Models/Tabellen
|
||||
|
||||
## Sell-Funktionen in `falukantService.js`
|
||||
|
||||
### 1. `sellProduct(hashedUserId, branchId, productId, quality, quantity)`
|
||||
Verkauft ein einzelnes Produkt mit bestimmter Qualität.
|
||||
|
||||
**Ablauf:**
|
||||
1. Lädt User, Branch, Character, Stock
|
||||
2. Lädt Inventory mit ProductType und Knowledge
|
||||
3. Berechnet Preis pro Einheit mit `calcRegionalSellPrice()`
|
||||
4. Berechnet kumulative Steuer mit politischen Befreiungen
|
||||
5. Passt Preis an (Inflation basierend auf Steuer)
|
||||
6. Berechnet Revenue, Tax, Net
|
||||
7. Aktualisiert Geld für Verkäufer und Treasury
|
||||
8. Entfernt verkaufte Items aus Inventory
|
||||
9. Erstellt/aktualisiert DaySell Eintrag
|
||||
10. Sendet Socket-Notifications
|
||||
|
||||
**Verwendete Models/Tabellen:**
|
||||
- `FalukantUser` (`falukant_data.falukant_user`)
|
||||
- `Branch` (`falukant_data.branch`)
|
||||
- `FalukantCharacter` (`falukant_data.character`)
|
||||
- `FalukantStock` (`falukant_data.stock`)
|
||||
- `Inventory` (`falukant_data.inventory`)
|
||||
- `ProductType` (`falukant_type.product`)
|
||||
- `Knowledge` (`falukant_data.knowledge`)
|
||||
- `TownProductWorth` (`falukant_data.town_product_worth`)
|
||||
- `RegionData` (`falukant_data.region`)
|
||||
- `RegionType` (`falukant_type.region`)
|
||||
- `PoliticalOffice` (`falukant_data.political_office`)
|
||||
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
|
||||
- `DaySell` (`falukant_log.day_sell`)
|
||||
- `MoneyFlow` (via `updateFalukantUserMoney`)
|
||||
|
||||
### 2. `sellAllProducts(hashedUserId, branchId)`
|
||||
Verkauft alle Produkte eines Branches.
|
||||
|
||||
**Ablauf:**
|
||||
1. Lädt User, Branch mit Stocks
|
||||
2. Lädt alle Inventory Items mit ProductType, Knowledge, Stock, Branch
|
||||
3. Für jedes Item:
|
||||
- Berechnet Preis pro Einheit
|
||||
- Berechnet kumulative Steuer
|
||||
- Passt Preis an
|
||||
- Erstellt/aktualisiert DaySell Eintrag
|
||||
4. Berechnet Gesamt-Tax pro Region
|
||||
5. Aktualisiert Geld für Verkäufer und Treasury
|
||||
6. Löscht alle Inventory Items
|
||||
7. Sendet Socket-Notifications
|
||||
|
||||
**Verwendete Models/Tabellen:**
|
||||
- `FalukantUser` (`falukant_data.falukant_user`)
|
||||
- `Branch` (`falukant_data.branch`)
|
||||
- `FalukantStock` (`falukant_data.stock`)
|
||||
- `FalukantStockType` (`falukant_type.stock`)
|
||||
- `FalukantCharacter` (`falukant_data.character`)
|
||||
- `Inventory` (`falukant_data.inventory`)
|
||||
- `ProductType` (`falukant_type.product`)
|
||||
- `Knowledge` (`falukant_data.knowledge`)
|
||||
- `TownProductWorth` (`falukant_data.town_product_worth`)
|
||||
- `RegionData` (`falukant_data.region`)
|
||||
- `RegionType` (`falukant_type.region`)
|
||||
- `PoliticalOffice` (`falukant_data.political_office`)
|
||||
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
|
||||
- `DaySell` (`falukant_log.day_sell`)
|
||||
- `MoneyFlow` (via `updateFalukantUserMoney`)
|
||||
|
||||
### 3. `addSellItem(branchId, userId, productId, quantity)`
|
||||
Erstellt oder aktualisiert einen DaySell Eintrag für einen Verkauf.
|
||||
|
||||
**Ablauf:**
|
||||
1. Lädt Branch
|
||||
2. Sucht nach existierendem DaySell Eintrag
|
||||
3. Erstellt neuen oder aktualisiert existierenden Eintrag
|
||||
|
||||
**Verwendete Models/Tabellen:**
|
||||
- `Branch` (`falukant_data.branch`)
|
||||
- `DaySell` (`falukant_log.day_sell`)
|
||||
|
||||
## Hilfsfunktionen
|
||||
|
||||
### `calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null)`
|
||||
Berechnet den Verkaufspreis eines Produkts basierend auf:
|
||||
- Basispreis (`product.sellCost`)
|
||||
- Regionalem Worth-Percent (`town_product_worth.worth_percent`)
|
||||
- Knowledge-Faktor (0-100)
|
||||
|
||||
**Verwendete Models/Tabellen:**
|
||||
- `ProductType` (`falukant_type.product`)
|
||||
- `TownProductWorth` (`falukant_data.town_product_worth`)
|
||||
|
||||
### `getCumulativeTaxPercent(regionId)`
|
||||
Berechnet die kumulative Steuer für eine Region und alle Vorfahren (rekursiv).
|
||||
|
||||
**SQL Query:**
|
||||
```sql
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id, parent_id, tax_percent
|
||||
FROM falukant_data.region r
|
||||
WHERE id = :id
|
||||
UNION ALL
|
||||
SELECT reg.id, reg.parent_id, reg.tax_percent
|
||||
FROM falukant_data.region reg
|
||||
JOIN ancestors a ON reg.id = a.parent_id
|
||||
)
|
||||
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;
|
||||
```
|
||||
|
||||
**Verwendete Tabellen:**
|
||||
- `falukant_data.region`
|
||||
|
||||
### `getCumulativeTaxPercentWithExemptions(userId, regionId)`
|
||||
Berechnet die kumulative Steuer mit politischen Befreiungen.
|
||||
|
||||
**Ablauf:**
|
||||
1. Lädt Character des Users
|
||||
2. Lädt alle PoliticalOffices des Characters
|
||||
3. Bestimmt befreite Region-Typen basierend auf Ämtern
|
||||
4. Berechnet kumulative Steuer, aber schließt befreite Region-Typen aus
|
||||
|
||||
**SQL Query:**
|
||||
```sql
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
|
||||
FROM falukant_data.region r
|
||||
JOIN falukant_type.region rt ON rt.id = r.region_type_id
|
||||
WHERE r.id = :id
|
||||
UNION ALL
|
||||
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
|
||||
FROM falukant_data.region reg
|
||||
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
|
||||
JOIN ancestors a ON reg.id = a.parent_id
|
||||
)
|
||||
SELECT COALESCE(SUM(CASE WHEN ARRAY[...] && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;
|
||||
```
|
||||
|
||||
**Verwendete Models/Tabellen:**
|
||||
- `FalukantCharacter` (`falukant_data.character`)
|
||||
- `PoliticalOffice` (`falukant_data.political_office`)
|
||||
- `PoliticalOfficeType` (`falukant_type.political_office_type`)
|
||||
- `RegionData` (`falukant_data.region`)
|
||||
- `RegionType` (`falukant_type.region`)
|
||||
|
||||
**Politische Steuerbefreiungen:**
|
||||
```javascript
|
||||
const POLITICAL_TAX_EXEMPTIONS = {
|
||||
'council': ['city'],
|
||||
'taxman': ['city', 'county'],
|
||||
'treasurerer': ['city', 'county', 'shire'],
|
||||
'super-state-administrator': ['city', 'county', 'shire', 'markgrave', 'duchy'],
|
||||
'chancellor': ['city','county','shire','markgrave','duchy'] // = alle Typen
|
||||
};
|
||||
```
|
||||
|
||||
## Model-Definitionen
|
||||
|
||||
### Inventory (`falukant_data.inventory`)
|
||||
```javascript
|
||||
// backend/models/falukant/data/inventory.js
|
||||
- id
|
||||
- stockId (FK zu falukant_data.stock)
|
||||
- productId (FK zu falukant_type.product)
|
||||
- quantity
|
||||
- quality
|
||||
- producedAt
|
||||
```
|
||||
|
||||
### DaySell (`falukant_log.day_sell`)
|
||||
```javascript
|
||||
// backend/models/falukant/log/daysell.js
|
||||
- id
|
||||
- regionId (FK zu falukant_data.region)
|
||||
- productId (FK zu falukant_type.product)
|
||||
- sellerId (FK zu falukant_data.falukant_user)
|
||||
- quantity
|
||||
- createdAt
|
||||
- updatedAt
|
||||
```
|
||||
|
||||
### TownProductWorth (`falukant_data.town_product_worth`)
|
||||
```javascript
|
||||
// backend/models/falukant/data/town_product_worth.js
|
||||
- id
|
||||
- productId (FK zu falukant_type.product)
|
||||
- regionId (FK zu falukant_data.region)
|
||||
- worthPercent (0-100)
|
||||
```
|
||||
|
||||
### Knowledge (`falukant_data.knowledge`)
|
||||
```javascript
|
||||
// backend/models/falukant/data/product_knowledge.js
|
||||
- id
|
||||
- productId (FK zu falukant_type.product)
|
||||
- characterId (FK zu falukant_data.character)
|
||||
- knowledge (0-99)
|
||||
```
|
||||
|
||||
## Wichtige SQL-Queries
|
||||
|
||||
### 1. Inventory mit ProductType und Knowledge laden
|
||||
```javascript
|
||||
Inventory.findAll({
|
||||
where: { quality },
|
||||
include: [
|
||||
{
|
||||
model: ProductType,
|
||||
as: 'productType',
|
||||
required: true,
|
||||
where: { id: productId },
|
||||
include: [
|
||||
{
|
||||
model: Knowledge,
|
||||
as: 'knowledges',
|
||||
required: false,
|
||||
where: { characterId: character.id }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Kumulative Steuer mit Befreiungen berechnen
|
||||
Siehe `getCumulativeTaxPercentWithExemptions()` oben.
|
||||
|
||||
## Preisberechnung
|
||||
|
||||
### Formel für `calcRegionalSellPrice`:
|
||||
1. Basispreis = `product.sellCost * (worthPercent / 100)`
|
||||
2. Min = `basePrice * 0.6`
|
||||
3. Max = `basePrice`
|
||||
4. Preis = `min + (max - min) * (knowledgeFactor / 100)`
|
||||
|
||||
### Steueranpassung:
|
||||
1. `inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100))`
|
||||
2. `adjustedPricePerUnit = pricePerUnit * inflationFactor`
|
||||
3. `revenue = quantity * adjustedPricePerUnit`
|
||||
4. `taxValue = revenue * cumulativeTax / 100`
|
||||
5. `net = revenue - taxValue`
|
||||
|
||||
## Vollständige Code-Snippets
|
||||
|
||||
### `calcRegionalSellPrice()`
|
||||
```javascript
|
||||
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
|
||||
if (worthPercent === null) {
|
||||
const townWorth = await TownProductWorth.findOne({
|
||||
where: { productId: product.id, regionId: regionId }
|
||||
});
|
||||
worthPercent = townWorth?.worthPercent || 50; // Default 50% wenn nicht gefunden
|
||||
}
|
||||
|
||||
// Basispreis basierend auf regionalem worthPercent
|
||||
const basePrice = product.sellCost * (worthPercent / 100);
|
||||
|
||||
// Dann Knowledge-Faktor anwenden
|
||||
const min = basePrice * 0.6;
|
||||
const max = basePrice;
|
||||
return min + (max - min) * (knowledgeFactor / 100);
|
||||
}
|
||||
```
|
||||
|
||||
### `getCumulativeTaxPercent()`
|
||||
```javascript
|
||||
async function getCumulativeTaxPercent(regionId) {
|
||||
if (!regionId) return 0;
|
||||
const rows = await sequelize.query(
|
||||
`WITH RECURSIVE ancestors AS (
|
||||
SELECT id, parent_id, tax_percent
|
||||
FROM falukant_data.region r
|
||||
WHERE id = :id
|
||||
UNION ALL
|
||||
SELECT reg.id, reg.parent_id, reg.tax_percent
|
||||
FROM falukant_data.region reg
|
||||
JOIN ancestors a ON reg.id = a.parent_id
|
||||
)
|
||||
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;`,
|
||||
{
|
||||
replacements: { id: regionId },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
const val = rows?.[0]?.total ?? 0;
|
||||
return parseFloat(val) || 0;
|
||||
}
|
||||
```
|
||||
|
||||
### `getCumulativeTaxPercentWithExemptions()` (vereinfacht)
|
||||
```javascript
|
||||
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
|
||||
if (!regionId) return 0;
|
||||
|
||||
// Character finden
|
||||
const character = await FalukantCharacter.findOne({
|
||||
where: { userId },
|
||||
attributes: ['id']
|
||||
});
|
||||
if (!character) return 0;
|
||||
|
||||
// Politische Ämter laden
|
||||
const offices = await PoliticalOffice.findAll({
|
||||
where: { characterId: character.id },
|
||||
include: [
|
||||
{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] },
|
||||
{
|
||||
model: RegionData,
|
||||
as: 'region',
|
||||
include: [{
|
||||
model: RegionType,
|
||||
as: 'regionType',
|
||||
attributes: ['labelTr']
|
||||
}]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Befreite Region-Typen bestimmen
|
||||
const exemptTypes = new Set();
|
||||
let hasChancellor = false;
|
||||
for (const o of offices) {
|
||||
const name = o.type?.name;
|
||||
if (!name) continue;
|
||||
if (name === 'chancellor') { hasChancellor = true; break; }
|
||||
const allowed = POLITICAL_TAX_EXEMPTIONS[name];
|
||||
if (allowed && Array.isArray(allowed)) {
|
||||
for (const t of allowed) exemptTypes.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChancellor) return 0;
|
||||
|
||||
// SQL Query mit Befreiungen
|
||||
const exemptTypesArray = Array.from(exemptTypes);
|
||||
const exemptTypesString = exemptTypesArray.length > 0
|
||||
? `ARRAY[${exemptTypesArray.map(t => `'${t.replace(/'/g, "''")}'`).join(',')}]`
|
||||
: `ARRAY[]::text[]`;
|
||||
|
||||
const rows = await sequelize.query(
|
||||
`WITH RECURSIVE ancestors AS (
|
||||
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
|
||||
FROM falukant_data.region r
|
||||
JOIN falukant_type.region rt ON rt.id = r.region_type_id
|
||||
WHERE r.id = :id
|
||||
UNION ALL
|
||||
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
|
||||
FROM falukant_data.region reg
|
||||
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
|
||||
JOIN ancestors a ON reg.id = a.parent_id
|
||||
)
|
||||
SELECT COALESCE(SUM(CASE WHEN ${exemptTypesString} && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;`,
|
||||
{
|
||||
replacements: { id: regionId },
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
const val = rows?.[0]?.total ?? 0;
|
||||
return parseFloat(val) || 0;
|
||||
}
|
||||
```
|
||||
|
||||
### `sellProduct()` (Kern-Logik)
|
||||
```javascript
|
||||
async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
|
||||
const user = await getFalukantUserOrFail(hashedUserId);
|
||||
const branch = await getBranchOrFail(user.id, branchId);
|
||||
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||
if (!character) throw new Error('No character found for user');
|
||||
const stock = await FalukantStock.findOne({ where: { branchId: branch.id } });
|
||||
if (!stock) throw new Error('Stock not found');
|
||||
|
||||
// Inventory laden
|
||||
const inventory = await Inventory.findAll({
|
||||
where: { quality },
|
||||
include: [
|
||||
{
|
||||
model: ProductType,
|
||||
as: 'productType',
|
||||
required: true,
|
||||
where: { id: productId },
|
||||
include: [
|
||||
{
|
||||
model: Knowledge,
|
||||
as: 'knowledges',
|
||||
required: false,
|
||||
where: { characterId: character.id }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!inventory.length) throw new Error('No inventory found');
|
||||
|
||||
const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
|
||||
if (available < quantity) throw new Error('Not enough inventory available');
|
||||
|
||||
const item = inventory[0].productType;
|
||||
const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
|
||||
const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
|
||||
|
||||
// Steuer berechnen
|
||||
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
|
||||
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
|
||||
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
|
||||
const revenue = quantity * adjustedPricePerUnit;
|
||||
|
||||
// Tax und Net berechnen
|
||||
const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
|
||||
const net = Math.round((revenue - taxValue) * 100) / 100;
|
||||
|
||||
// Geld aktualisieren
|
||||
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id);
|
||||
if (!moneyResult.success) throw new Error('Failed to update money for seller');
|
||||
|
||||
// Steuer an Treasury buchen
|
||||
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
|
||||
if (treasuryId && taxValue > 0) {
|
||||
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id);
|
||||
if (!taxResult.success) throw new Error('Failed to update money for treasury');
|
||||
}
|
||||
|
||||
// Inventory aktualisieren
|
||||
let remaining = quantity;
|
||||
for (const inv of inventory) {
|
||||
if (inv.quantity <= remaining) {
|
||||
remaining -= inv.quantity;
|
||||
await inv.destroy();
|
||||
} else {
|
||||
await inv.update({ quantity: inv.quantity - remaining });
|
||||
remaining = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// DaySell Eintrag erstellen/aktualisieren
|
||||
await this.addSellItem(branchId, user.id, productId, quantity);
|
||||
|
||||
// Notifications senden
|
||||
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
|
||||
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
### `addSellItem()`
|
||||
```javascript
|
||||
async addSellItem(branchId, userId, productId, quantity) {
|
||||
const branch = await Branch.findOne({
|
||||
where: { id: branchId },
|
||||
});
|
||||
const daySell = await DaySell.findOne({
|
||||
where: {
|
||||
regionId: branch.regionId,
|
||||
productId: productId,
|
||||
sellerId: userId,
|
||||
}
|
||||
});
|
||||
if (daySell) {
|
||||
daySell.quantity += quantity;
|
||||
await daySell.save();
|
||||
} else {
|
||||
await DaySell.create({
|
||||
regionId: branch.regionId,
|
||||
productId: productId,
|
||||
sellerId: userId,
|
||||
quantity: quantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
1. **Inventory wird nach Verkauf gelöscht/aktualisiert**: Items werden aus der Inventory entfernt oder die Menge reduziert.
|
||||
|
||||
2. **DaySell wird aggregiert**: Wenn bereits ein DaySell Eintrag für Region/Product/Seller existiert, wird die Menge addiert.
|
||||
|
||||
3. **Steuer wird an Treasury gebucht**: Wenn `TREASURY_FALUKANT_USER_ID` gesetzt ist, wird die Steuer an diesen User gebucht.
|
||||
|
||||
4. **Socket-Notifications**: Nach jedem Verkauf werden `falukantUpdateStatus` und `falukantBranchUpdate` Events gesendet.
|
||||
|
||||
5. **Politische Befreiungen**: Bestimmte politische Ämter befreien von Steuern in bestimmten Region-Typen. Chancellor befreit von allen Steuern.
|
||||
|
||||
6. **Preis-Inflation**: Der Preis wird basierend auf der Steuer inflatiert, damit der Netto-Betrag für den Verkäufer gleich bleibt.
|
||||
|
||||
## Tabellenübersicht
|
||||
|
||||
### `falukant_data.inventory`
|
||||
- `id` (PK)
|
||||
- `stock_id` (FK zu `falukant_data.stock`)
|
||||
- `product_id` (FK zu `falukant_type.product`)
|
||||
- `quantity` (INTEGER)
|
||||
- `quality` (INTEGER)
|
||||
- `produced_at` (DATE)
|
||||
|
||||
### `falukant_log.sell` (DaySell)
|
||||
- `id` (PK)
|
||||
- `region_id` (FK zu `falukant_data.region`)
|
||||
- `product_id` (FK zu `falukant_type.product`)
|
||||
- `seller_id` (FK zu `falukant_data.falukant_user`)
|
||||
- `quantity` (INTEGER)
|
||||
- `sell_timestamp` (DATE)
|
||||
- **Unique Index**: `(seller_id, product_id, region_id)`
|
||||
|
||||
### `falukant_data.town_product_worth`
|
||||
- `id` (PK)
|
||||
- `product_id` (FK zu `falukant_type.product`)
|
||||
- `region_id` (FK zu `falukant_data.region`)
|
||||
- `worth_percent` (INTEGER, 0-100)
|
||||
|
||||
### `falukant_data.knowledge`
|
||||
- `id` (PK)
|
||||
- `product_id` (FK zu `falukant_type.product`)
|
||||
- `character_id` (FK zu `falukant_data.character`)
|
||||
- `knowledge` (INTEGER, 0-99)
|
||||
|
||||
### `falukant_data.political_office`
|
||||
- `id` (PK)
|
||||
- `office_type_id` (FK zu `falukant_type.political_office_type`)
|
||||
- `character_id` (FK zu `falukant_data.character`)
|
||||
- `region_id` (FK zu `falukant_data.region`)
|
||||
- `created_at`, `updated_at`
|
||||
|
||||
### `falukant_type.political_office_type`
|
||||
- `id` (PK)
|
||||
- `name` (STRING) - z.B. 'council', 'taxman', 'treasurerer', 'super-state-administrator', 'chancellor'
|
||||
- `seats_per_region` (INTEGER)
|
||||
- `region_type` (STRING)
|
||||
- `term_length` (INTEGER)
|
||||
|
||||
### `falukant_data.region`
|
||||
- `id` (PK)
|
||||
- `name` (STRING)
|
||||
- `region_type_id` (FK zu `falukant_type.region`)
|
||||
- `parent_id` (FK zu `falukant_data.region`, nullable)
|
||||
- `map` (JSONB)
|
||||
- `tax_percent` (DECIMAL)
|
||||
|
||||
### `falukant_type.region`
|
||||
- `id` (PK)
|
||||
- `label_tr` (STRING) - z.B. 'city', 'county', 'shire', 'markgrave', 'duchy'
|
||||
- `parent_id` (FK zu `falukant_type.region`, nullable)
|
||||
|
||||
### `falukant_data.falukant_user`
|
||||
- `id` (PK)
|
||||
- `user_id` (FK zu `community.user`)
|
||||
- `money` (DECIMAL)
|
||||
- `credit_amount`, `today_credit_taken`, `credit_interest_rate`
|
||||
- `certificate`
|
||||
- `main_branch_region_id`
|
||||
- `last_nobility_advance_at`
|
||||
- `created_at`, `updated_at`
|
||||
|
||||
### `falukant_data.character`
|
||||
- `id` (PK)
|
||||
- `user_id` (FK zu `falukant_data.falukant_user`)
|
||||
- `region_id` (FK zu `falukant_data.region`)
|
||||
- `first_name`, `last_name`
|
||||
- `birthdate`, `gender`, `health`
|
||||
- `title_of_nobility` (FK zu `falukant_type.title_of_nobility`)
|
||||
- `mood_id` (FK zu `falukant_type.mood`)
|
||||
- `created_at`, `updated_at`
|
||||
|
||||
### `falukant_data.branch`
|
||||
- `id` (PK)
|
||||
- `branch_type_id` (FK zu `falukant_type.branch`)
|
||||
- `region_id` (FK zu `falukant_data.region`)
|
||||
- `falukant_user_id` (FK zu `falukant_data.falukant_user`)
|
||||
|
||||
### `falukant_data.stock`
|
||||
- `id` (PK)
|
||||
- `branch_id` (FK zu `falukant_data.branch`)
|
||||
- `stock_type_id` (FK zu `falukant_type.stock`)
|
||||
- `quantity` (INTEGER)
|
||||
- `product_quality` (INTEGER, nullable)
|
||||
|
||||
### `falukant_type.product`
|
||||
- `id` (PK)
|
||||
- `label_tr` (STRING, unique)
|
||||
- `category` (INTEGER)
|
||||
- `production_time` (INTEGER)
|
||||
- `sell_cost` (INTEGER)
|
||||
|
||||
## Dateipfade
|
||||
|
||||
- **Service**: `backend/services/falukantService.js`
|
||||
- **Models**:
|
||||
- `backend/models/falukant/data/inventory.js`
|
||||
- `backend/models/falukant/log/daysell.js`
|
||||
- `backend/models/falukant/data/town_product_worth.js`
|
||||
- `backend/models/falukant/data/product_knowledge.js`
|
||||
- `backend/models/falukant/data/political_office.js`
|
||||
- `backend/models/falukant/type/political_office_type.js`
|
||||
- `backend/models/falukant/data/region.js`
|
||||
- `backend/models/falukant/type/region.js`
|
||||
- `backend/models/falukant/data/character.js`
|
||||
- `backend/models/falukant/data/user.js`
|
||||
- `backend/models/falukant/data/branch.js`
|
||||
- `backend/models/falukant/data/stock.js`
|
||||
- `backend/models/falukant/type/product.js`
|
||||
|
||||
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();
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import crypto from 'crypto';
|
||||
import chatRouter from './routers/chatRouter.js';
|
||||
import authRouter from './routers/authRouter.js';
|
||||
import navigationRouter from './routers/navigationRouter.js';
|
||||
@@ -11,12 +12,17 @@ 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';
|
||||
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';
|
||||
|
||||
@@ -25,16 +31,59 @@ const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
|
||||
// Request-Timing (aktivierbar per ENV)
|
||||
// - LOG_SLOW_REQ_MS=200: Logge Requests, die länger dauern als X ms (Default 500)
|
||||
// - 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;
|
||||
res.setHeader('x-request-id', reqId);
|
||||
const t0 = Date.now();
|
||||
res.on('finish', () => {
|
||||
const ms = Date.now() - t0;
|
||||
if (LOG_ALL_REQ || ms >= LOG_SLOW_REQ_MS) {
|
||||
console.log(`⏱️ REQ ${ms}ms ${res.statusCode} ${req.method} ${req.originalUrl} rid=${reqId}`);
|
||||
}
|
||||
});
|
||||
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);
|
||||
@@ -49,14 +98,26 @@ app.use('/api/taxi/highscores', taxiHighscoreRouter);
|
||||
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
|
||||
app.use('/api/contact', contactRouter);
|
||||
app.use('/api/socialnetwork', socialnetworkRouter);
|
||||
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
|
||||
|
||||
@@ -43,6 +43,9 @@ class AdminController {
|
||||
this.getRegionDistances = this.getRegionDistances.bind(this);
|
||||
this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
|
||||
this.deleteRegionDistance = this.deleteRegionDistance.bind(this);
|
||||
this.createNPCs = this.createNPCs.bind(this);
|
||||
this.getTitlesOfNobility = this.getTitlesOfNobility.bind(this);
|
||||
this.getNPCsCreationStatus = this.getNPCsCreationStatus.bind(this);
|
||||
}
|
||||
|
||||
async getOpenInterests(req, res) {
|
||||
@@ -383,6 +386,59 @@ class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
async createNPCs(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const { regionIds, minAge, maxAge, minTitleId, maxTitleId, count } = req.body;
|
||||
const countValue = parseInt(count) || 1;
|
||||
if (countValue < 1 || countValue > 500) {
|
||||
return res.status(400).json({ error: 'Count must be between 1 and 500' });
|
||||
}
|
||||
console.log('[createNPCs] Request received:', { userId, regionIds, minAge, maxAge, minTitleId, maxTitleId, count: countValue });
|
||||
const result = await AdminService.createNPCs(userId, {
|
||||
regionIds: regionIds && regionIds.length > 0 ? regionIds : null,
|
||||
minAge: parseInt(minAge) || 0,
|
||||
maxAge: parseInt(maxAge) || 100,
|
||||
minTitleId: parseInt(minTitleId) || 1,
|
||||
maxTitleId: parseInt(maxTitleId) || 19,
|
||||
count: countValue
|
||||
});
|
||||
console.log('[createNPCs] Job created:', result);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[createNPCs] Error:', error);
|
||||
console.error('[createNPCs] Error stack:', error.stack);
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async getTitlesOfNobility(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const titles = await AdminService.getTitlesOfNobility(userId);
|
||||
res.status(200).json(titles);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getNPCsCreationStatus(req, res) {
|
||||
try {
|
||||
const { userid: userId } = req.headers;
|
||||
const { jobId } = req.params;
|
||||
const status = await AdminService.getNPCsCreationStatus(userId, jobId);
|
||||
res.status(200).json(status);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const status = error.message === 'noaccess' || error.message === 'Access denied' ? 403 :
|
||||
error.message === 'Job not found' ? 404 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getRoomTypes(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
|
||||
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,45 +26,51 @@ 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.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId), { blockInDebtorsPrison: true });
|
||||
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
|
||||
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch));
|
||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
|
||||
this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId), { blockInDebtorsPrison: true });
|
||||
this.createProduction = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, productId, quantity } = req.body;
|
||||
return this.service.createProduction(userId, branchId, productId, quantity);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getProduction = this._wrapWithUser((userId, req) => this.service.getProduction(userId, req.params.branchId));
|
||||
this.getStock = this._wrapWithUser((userId, req) => this.service.getStock(userId, req.params.branchId || null));
|
||||
this.createStock = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, stockTypeId, stockSize } = req.body;
|
||||
return this.service.createStock(userId, branchId, stockTypeId, stockSize);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId));
|
||||
this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId));
|
||||
this.sellProduct = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, productId, quality, quantity } = req.body;
|
||||
return this.service.sellProduct(userId, branchId, productId, quality, quantity);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.sellAllProducts = this._wrapWithUser((userId, req) => {
|
||||
const { branchId } = req.body;
|
||||
return this.service.sellAllProducts(userId, branchId);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.moneyHistory = this._wrapWithUser((userId, req) => {
|
||||
let { page, filter } = req.body;
|
||||
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;
|
||||
return this.service.buyStorage(userId, branchId, amount, stockTypeId);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.sellStorage = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, amount, stockTypeId } = req.body;
|
||||
return this.service.sellStorage(userId, branchId, amount, stockTypeId);
|
||||
}, { successStatus: 202 });
|
||||
}, { successStatus: 202, blockInDebtorsPrison: true });
|
||||
|
||||
this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes());
|
||||
this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview());
|
||||
@@ -74,75 +80,137 @@ class FalukantController {
|
||||
console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId);
|
||||
return this.service.getDirectorProposals(userId, req.body.branchId);
|
||||
});
|
||||
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId));
|
||||
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
||||
this.getDirectorForBranch = this._wrapWithUser((userId, req) => this.service.getDirectorForBranch(userId, req.params.branchId));
|
||||
this.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId));
|
||||
this.updateDirector = this._wrapWithUser((userId, req) => {
|
||||
const { directorId, income } = req.body;
|
||||
return this.service.updateDirector(userId, directorId, income);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.setSetting = this._wrapWithUser((userId, req) => {
|
||||
const { branchId, directorId, settingKey, value } = req.body;
|
||||
return this.service.setSetting(userId, branchId, directorId, settingKey, value);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getFamily = this._wrapWithUser(async (userId) => {
|
||||
const result = await this.service.getFamily(userId);
|
||||
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.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
|
||||
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
|
||||
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true });
|
||||
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true });
|
||||
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { blockInDebtorsPrison: true });
|
||||
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, blockInDebtorsPrison: true });
|
||||
this.getGifts = this._wrapWithUser((userId) => {
|
||||
console.log('🔍 getGifts called with userId:', userId);
|
||||
return this.service.getGifts(userId);
|
||||
});
|
||||
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
|
||||
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
|
||||
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
|
||||
this.service.spendTimeWithSpouse(userId), { blockInDebtorsPrison: true });
|
||||
this.giftToSpouse = this._wrapWithUser((userId, req) =>
|
||||
this.service.giftToSpouse(userId, req.body?.giftLevel), { blockInDebtorsPrison: true });
|
||||
this.reconcileMarriage = this._wrapWithUser((userId) =>
|
||||
this.service.reconcileMarriage(userId), { blockInDebtorsPrison: true });
|
||||
this.acknowledgeLover = this._wrapWithUser((userId, req) =>
|
||||
this.service.acknowledgeLover(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
||||
this.endLoverRelationship = this._wrapWithUser((userId, req) =>
|
||||
this.service.endLoverRelationship(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
|
||||
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;
|
||||
}
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
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, blockInDebtorsPrison: true });
|
||||
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));
|
||||
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
|
||||
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(userId));
|
||||
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201 });
|
||||
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.dismissServants = this._wrapWithUser((userId, req) => this.service.dismissServants(userId, req.body?.amount), { blockInDebtorsPrison: true });
|
||||
this.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel), { blockInDebtorsPrison: true });
|
||||
this.tidyHousehold = this._wrapWithUser((userId) => this.service.tidyHousehold(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
|
||||
this.createParty = this._wrapWithUser((userId, req) => {
|
||||
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
|
||||
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
|
||||
|
||||
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);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
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);
|
||||
}, { blockInDebtorsPrison: true });
|
||||
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
|
||||
const { applicationId, decision } = req.body;
|
||||
return this.service.decideOnChurchApplication(userId, applicationId, decision);
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
|
||||
this.sendToSchool = this._wrapWithUser((userId, req) => {
|
||||
const { item, student, studentId } = req.body;
|
||||
return this.service.sendToSchool(userId, item, student, studentId);
|
||||
});
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId));
|
||||
this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId));
|
||||
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height));
|
||||
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId));
|
||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
|
||||
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
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;
|
||||
}
|
||||
}, { blockInDebtorsPrison: true });
|
||||
|
||||
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
|
||||
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
|
||||
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
|
||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes));
|
||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
|
||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
|
||||
@@ -154,6 +222,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);
|
||||
@@ -163,13 +238,26 @@ class FalukantController {
|
||||
}
|
||||
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
|
||||
});
|
||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||
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), { blockInDebtorsPrison: true });
|
||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
|
||||
|
||||
this.getUndergroundTypes = this._wrapWithUser((userId) => this.service.getUndergroundTypes(userId));
|
||||
this.getRaidTransportRegions = this._wrapWithUser((userId) => this.service.getRaidTransportRegions(userId));
|
||||
this.getUndergroundActivities = this._wrapWithUser((userId) => this.service.getUndergroundActivities(userId));
|
||||
this.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
|
||||
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
|
||||
this.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) => {
|
||||
@@ -190,7 +278,7 @@ class FalukantController {
|
||||
throw { status: 400, message: 'goal is required for corrupt_politician' };
|
||||
}
|
||||
return this.service.createUndergroundActivity(userId, payload);
|
||||
}, { successStatus: 201 });
|
||||
}, { successStatus: 201, blockInDebtorsPrison: true });
|
||||
|
||||
this.getUndergroundAttacks = this._wrapWithUser((userId, req) => {
|
||||
const direction = (req.query.direction || '').toLowerCase();
|
||||
@@ -204,14 +292,14 @@ class FalukantController {
|
||||
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
|
||||
this.buyVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.buyVehicles(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.getVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
|
||||
);
|
||||
this.createTransport = this._wrapWithUser(
|
||||
(userId, req) => this.service.createTransport(userId, req.body),
|
||||
{ successStatus: 201 }
|
||||
{ successStatus: 201, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.getTransportRoute = this._wrapWithUser(
|
||||
(userId, req) => this.service.getTransportRoute(userId, req.query)
|
||||
@@ -221,30 +309,39 @@ class FalukantController {
|
||||
);
|
||||
this.repairVehicle = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
|
||||
{ successStatus: 200 }
|
||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
||||
);
|
||||
this.repairAllVehicles = this._wrapWithUser(
|
||||
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
|
||||
{ successStatus: 200 }
|
||||
{ successStatus: 200, blockInDebtorsPrison: true }
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
_wrapWithUser(fn, { successStatus = 200, postProcess } = {}) {
|
||||
_wrapWithUser(fn, { successStatus = 200, postProcess, blockInDebtorsPrison = false } = {}) {
|
||||
return async (req, res) => {
|
||||
try {
|
||||
const hashedUserId = extractHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(400).json({ error: 'Missing user identifier' });
|
||||
}
|
||||
if (blockInDebtorsPrison) {
|
||||
await this.service.assertActionAllowedOutsideDebtorsPrison(hashedUserId);
|
||||
}
|
||||
const result = await fn(hashedUserId, req, res);
|
||||
const toSend = postProcess ? postProcess(result) : result;
|
||||
res.status(successStatus).json(toSend);
|
||||
} 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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import UserRight from '../models/community/user_right.js';
|
||||
import UserRightType from '../models/type/user_right.js';
|
||||
import UserParamType from '../models/type/user_param.js';
|
||||
import FalukantUser from '../models/falukant/data/user.js';
|
||||
import VocabService from '../services/vocabService.js';
|
||||
|
||||
const menuStructure = {
|
||||
home: {
|
||||
@@ -177,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",
|
||||
@@ -274,6 +299,10 @@ const menuStructure = {
|
||||
visible: ["mainadmin", "falukant"],
|
||||
path: "/admin/falukant/map"
|
||||
},
|
||||
createNPC: {
|
||||
visible: ["mainadmin", "falukant"],
|
||||
path: "/admin/falukant/create-npc"
|
||||
},
|
||||
}
|
||||
},
|
||||
minigames: {
|
||||
@@ -296,6 +325,7 @@ const menuStructure = {
|
||||
class NavigationController {
|
||||
constructor() {
|
||||
this.menu = this.menu.bind(this);
|
||||
this.vocabService = new VocabService();
|
||||
}
|
||||
|
||||
calculateAge(birthDate) {
|
||||
@@ -365,6 +395,11 @@ class NavigationController {
|
||||
const age = this.calculateAge(birthDate);
|
||||
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
|
||||
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
|
||||
|
||||
// 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) {
|
||||
console.error('Error fetching menu:', 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.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
80
backend/controllers/vocabController.js
Normal file
80
backend/controllers/vocabController.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import VocabService from '../services/vocabService.js';
|
||||
|
||||
function extractHashedUserId(req) {
|
||||
return req.headers?.userid;
|
||||
}
|
||||
|
||||
class VocabController {
|
||||
constructor() {
|
||||
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));
|
||||
|
||||
this.listChapters = this._wrapWithUser((userId, req) => this.service.listChapters(userId, req.params.languageId));
|
||||
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 });
|
||||
this.listLanguageVocabs = this._wrapWithUser((userId, req) => this.service.listLanguageVocabs(userId, req.params.languageId));
|
||||
this.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
|
||||
|
||||
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 } = {}) {
|
||||
return async (req, res) => {
|
||||
try {
|
||||
const hashedUserId = extractHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(400).json({ error: 'Missing user identifier' });
|
||||
}
|
||||
const result = await fn(hashedUserId, req, res);
|
||||
res.status(successStatus).json(result);
|
||||
} 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' });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default VocabController;
|
||||
|
||||
|
||||
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();
|
||||
@@ -1,14 +1,39 @@
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
|
||||
const PORT = 4551;
|
||||
const PORT = Number.parseInt(process.env.DAEMON_PORT || '4551', 10);
|
||||
const USE_TLS = process.env.DAEMON_TLS === '1';
|
||||
const TLS_KEY_PATH = process.env.DAEMON_TLS_KEY_PATH;
|
||||
const TLS_CERT_PATH = process.env.DAEMON_TLS_CERT_PATH;
|
||||
const TLS_CA_PATH = process.env.DAEMON_TLS_CA_PATH; // optional
|
||||
|
||||
// Einfache In-Memory-Struktur für Verbindungen (für spätere Erweiterungen)
|
||||
const connections = new Set();
|
||||
|
||||
function createServer() {
|
||||
const wss = new WebSocketServer({ port: PORT });
|
||||
let wss;
|
||||
|
||||
console.log(`[Daemon] WebSocket-Server startet auf Port ${PORT} ...`);
|
||||
if (USE_TLS) {
|
||||
if (!TLS_KEY_PATH || !TLS_CERT_PATH) {
|
||||
console.error('[Daemon] DAEMON_TLS=1 gesetzt, aber DAEMON_TLS_KEY_PATH/DAEMON_TLS_CERT_PATH fehlen.');
|
||||
process.exit(1);
|
||||
}
|
||||
const 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,
|
||||
});
|
||||
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 {
|
||||
// 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} ...`);
|
||||
}
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const peer = req.socket.remoteAddress + ':' + req.socket.remotePort;
|
||||
|
||||
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,79 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// falukant_data.character.reputation (integer, default random 20..80)
|
||||
// Wichtig: Schema explizit angeben
|
||||
// Vorgehen:
|
||||
// - Spalte anlegen (falls noch nicht vorhanden)
|
||||
// - bestehende Zeilen initialisieren (random 20..80)
|
||||
// - DEFAULT setzen (random 20..80)
|
||||
// - NOT NULL + CHECK 0..100 erzwingen
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'character'
|
||||
AND column_name = 'reputation'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data."character"
|
||||
ADD COLUMN reputation integer;
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
|
||||
// Backfill: nur NULLs initialisieren (damit bestehende Werte nicht überschrieben werden)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE falukant_data."character"
|
||||
SET reputation = (floor(random()*61)+20)::int
|
||||
WHERE reputation IS NULL;
|
||||
`);
|
||||
|
||||
// DEFAULT + NOT NULL (nach Backfill)
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data."character"
|
||||
ALTER COLUMN reputation SET DEFAULT (floor(random()*61)+20)::int;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data."character"
|
||||
ALTER COLUMN reputation SET NOT NULL;
|
||||
`);
|
||||
|
||||
// Enforce 0..100 at DB level (percent)
|
||||
// (IF NOT EXISTS pattern, because deployments can be re-run)
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE c.conname = 'character_reputation_0_100_chk'
|
||||
AND n.nspname = 'falukant_data'
|
||||
AND t.relname = 'character'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data."character"
|
||||
ADD CONSTRAINT character_reputation_0_100_chk
|
||||
CHECK (reputation >= 0 AND reputation <= 100);
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data."character"
|
||||
DROP CONSTRAINT IF EXISTS character_reputation_0_100_chk;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data."character"
|
||||
DROP COLUMN IF EXISTS reputation;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
47
backend/migrations/20251220001000-add-reputation-actions.cjs
Normal file
47
backend/migrations/20251220001000-add-reputation-actions.cjs
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Typ-Tabelle (konfigurierbar ohne Code): falukant_type.reputation_action
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS falukant_type.reputation_action (
|
||||
id serial PRIMARY KEY,
|
||||
tr text NOT NULL UNIQUE,
|
||||
cost integer NOT NULL CHECK (cost >= 0),
|
||||
base_gain integer NOT NULL CHECK (base_gain >= 0),
|
||||
decay_factor double precision NOT NULL CHECK (decay_factor > 0 AND decay_factor <= 1),
|
||||
min_gain integer NOT NULL DEFAULT 0 CHECK (min_gain >= 0),
|
||||
decay_window_days integer NOT NULL DEFAULT 7 CHECK (decay_window_days >= 1 AND decay_window_days <= 365)
|
||||
);
|
||||
`);
|
||||
|
||||
// Log-Tabelle: falukant_log.reputation_action
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS falukant_log.reputation_action (
|
||||
id serial PRIMARY KEY,
|
||||
falukant_user_id integer NOT NULL,
|
||||
action_type_id integer NOT NULL,
|
||||
cost integer NOT NULL CHECK (cost >= 0),
|
||||
base_gain integer NOT NULL CHECK (base_gain >= 0),
|
||||
gain integer NOT NULL CHECK (gain >= 0),
|
||||
times_used_before integer NOT NULL CHECK (times_used_before >= 0),
|
||||
action_timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS reputation_action_log_user_type_idx
|
||||
ON falukant_log.reputation_action (falukant_user_id, action_type_id);
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS reputation_action_log_ts_idx
|
||||
ON falukant_log.reputation_action (action_timestamp);
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_log.reputation_action;`);
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS falukant_type.reputation_action;`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Für bereits existierende Installationen: Spalte sicherstellen + Backfill
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_type.reputation_action
|
||||
ADD COLUMN IF NOT EXISTS decay_window_days integer;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE falukant_type.reputation_action
|
||||
SET decay_window_days = 7
|
||||
WHERE decay_window_days IS NULL;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_type.reputation_action
|
||||
ALTER COLUMN decay_window_days SET DEFAULT 7;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_type.reputation_action
|
||||
ALTER COLUMN decay_window_days SET NOT NULL;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_type.reputation_action
|
||||
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_type.reputation_action
|
||||
ADD CONSTRAINT reputation_action_decay_window_days_chk
|
||||
CHECK (decay_window_days >= 1 AND decay_window_days <= 365);
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// optional: wieder entfernen
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_type.reputation_action
|
||||
DROP CONSTRAINT IF EXISTS reputation_action_decay_window_days_chk;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_type.reputation_action
|
||||
DROP COLUMN IF EXISTS decay_window_days;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Idempotentes Seed: legt Ruf-Aktionen an bzw. aktualisiert sie anhand "tr"
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO falukant_type.reputation_action
|
||||
(tr, cost, base_gain, decay_factor, min_gain, decay_window_days)
|
||||
VALUES
|
||||
('soup_kitchen', 500, 2, 0.85, 0, 7),
|
||||
('library_donation', 5000, 4, 0.88, 0, 7),
|
||||
('well_build', 8000, 4, 0.87, 0, 7),
|
||||
('scholarships', 10000, 5, 0.87, 0, 7),
|
||||
('church_hospice', 12000, 5, 0.87, 0, 7),
|
||||
('school_funding', 15000, 6, 0.88, 0, 7),
|
||||
('orphanage_build', 20000, 7, 0.90, 0, 7),
|
||||
('bridge_build', 25000, 7, 0.90, 0, 7),
|
||||
('hospital_donation', 30000, 8, 0.90, 0, 7),
|
||||
('patronage', 40000, 9, 0.91, 0, 7),
|
||||
('statue_build', 50000, 10, 0.92, 0, 7)
|
||||
ON CONFLICT (tr) DO UPDATE SET
|
||||
cost = EXCLUDED.cost,
|
||||
base_gain = EXCLUDED.base_gain,
|
||||
decay_factor = EXCLUDED.decay_factor,
|
||||
min_gain = EXCLUDED.min_gain,
|
||||
decay_window_days = EXCLUDED.decay_window_days;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Entfernt nur die gesetzten Seeds (tr-basiert)
|
||||
await queryInterface.sequelize.query(`
|
||||
DELETE FROM falukant_type.reputation_action
|
||||
WHERE tr IN (
|
||||
'soup_kitchen',
|
||||
'library_donation',
|
||||
'well_build',
|
||||
'scholarships',
|
||||
'church_hospice',
|
||||
'school_funding',
|
||||
'orphanage_build',
|
||||
'bridge_build',
|
||||
'hospital_donation',
|
||||
'patronage',
|
||||
'statue_build'
|
||||
);
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Ensure column exists
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
ADD COLUMN IF NOT EXISTS condition integer;
|
||||
`);
|
||||
|
||||
// Backfill nulls (legacy data)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE falukant_data.vehicle
|
||||
SET condition = 100
|
||||
WHERE condition IS NULL;
|
||||
`);
|
||||
|
||||
// Clamp out-of-range values defensively
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE falukant_data.vehicle
|
||||
SET condition = GREATEST(0, LEAST(100, condition))
|
||||
WHERE condition < 0 OR condition > 100;
|
||||
`);
|
||||
|
||||
// Default + NOT NULL
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
ALTER COLUMN condition SET DEFAULT 100;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
ALTER COLUMN condition SET NOT NULL;
|
||||
`);
|
||||
|
||||
// Check constraint 0..100
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
ADD CONSTRAINT vehicle_condition_0_100_chk
|
||||
CHECK (condition >= 0 AND condition <= 100);
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Keep the column, but remove constraint/default to be reversible
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
DROP CONSTRAINT IF EXISTS vehicle_condition_0_100_chk;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.vehicle
|
||||
ALTER COLUMN condition DROP DEFAULT;
|
||||
`);
|
||||
// NOT NULL not reverted to avoid introducing NULLs on rollback; can be adjusted if needed
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.director
|
||||
ADD COLUMN IF NOT EXISTS may_repair_vehicles boolean;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE falukant_data.director
|
||||
SET may_repair_vehicles = true
|
||||
WHERE may_repair_vehicles IS NULL;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.director
|
||||
ALTER COLUMN may_repair_vehicles SET DEFAULT true;
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.director
|
||||
ALTER COLUMN may_repair_vehicles SET NOT NULL;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// optional rollback: drop column
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.director
|
||||
DROP COLUMN IF EXISTS may_repair_vehicles;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/* eslint-disable */
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
// Sprache / Set, das geteilt werden kann
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_language (
|
||||
id SERIAL PRIMARY KEY,
|
||||
owner_user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
share_code TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_language_owner_fk
|
||||
FOREIGN KEY (owner_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_language_share_code_uniq UNIQUE (share_code)
|
||||
);
|
||||
`);
|
||||
|
||||
// Abos (Freunde)
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_language_subscription (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
language_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_language_subscription_user_fk
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_language_subscription_language_fk
|
||||
FOREIGN KEY (language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_language_subscription_uniq UNIQUE (user_id, language_id)
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_language_owner_idx
|
||||
ON community.vocab_language(owner_user_id);
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_language_subscription_user_idx
|
||||
ON community.vocab_language_subscription(user_id);
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_language_subscription_language_idx
|
||||
ON community.vocab_language_subscription(language_id);
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language_subscription;`);
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_language;`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable */
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
// Kapitel innerhalb einer Sprache
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_chapter (
|
||||
id SERIAL PRIMARY KEY,
|
||||
language_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_chapter_language_fk
|
||||
FOREIGN KEY (language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_chapter_creator_fk
|
||||
FOREIGN KEY (created_by_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_chapter_language_idx
|
||||
ON community.vocab_chapter(language_id);
|
||||
`);
|
||||
|
||||
// Lexeme/Wörter (wir deduplizieren pro Sprache über normalized)
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_lexeme (
|
||||
id SERIAL PRIMARY KEY,
|
||||
language_id INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
normalized TEXT NOT NULL,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_lexeme_language_fk
|
||||
FOREIGN KEY (language_id)
|
||||
REFERENCES community.vocab_language(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_lexeme_creator_fk
|
||||
FOREIGN KEY (created_by_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_lexeme_unique_per_language UNIQUE (language_id, normalized)
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_lexeme_language_idx
|
||||
ON community.vocab_lexeme(language_id);
|
||||
`);
|
||||
|
||||
// n:m Zuordnung pro Kapitel: Lernwort ↔ Referenzwort (Mehrdeutigkeiten möglich)
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS community.vocab_chapter_lexeme (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chapter_id INTEGER NOT NULL,
|
||||
learning_lexeme_id INTEGER NOT NULL,
|
||||
reference_lexeme_id INTEGER NOT NULL,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT vocab_chlex_chapter_fk
|
||||
FOREIGN KEY (chapter_id)
|
||||
REFERENCES community.vocab_chapter(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_chlex_learning_fk
|
||||
FOREIGN KEY (learning_lexeme_id)
|
||||
REFERENCES community.vocab_lexeme(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_chlex_reference_fk
|
||||
FOREIGN KEY (reference_lexeme_id)
|
||||
REFERENCES community.vocab_lexeme(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_chlex_creator_fk
|
||||
FOREIGN KEY (created_by_user_id)
|
||||
REFERENCES community."user"(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT vocab_chlex_unique UNIQUE (chapter_id, learning_lexeme_id, reference_lexeme_id)
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_chlex_chapter_idx
|
||||
ON community.vocab_chapter_lexeme(chapter_id);
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_chlex_learning_idx
|
||||
ON community.vocab_chapter_lexeme(learning_lexeme_id);
|
||||
`);
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx
|
||||
ON community.vocab_chapter_lexeme(reference_lexeme_id);
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter_lexeme;`);
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_lexeme;`);
|
||||
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS community.vocab_chapter;`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS falukant_data.relationship_state (
|
||||
id serial PRIMARY KEY,
|
||||
relationship_id integer NOT NULL UNIQUE,
|
||||
marriage_satisfaction integer NOT NULL DEFAULT 55 CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100),
|
||||
marriage_public_stability integer NOT NULL DEFAULT 55 CHECK (marriage_public_stability >= 0 AND marriage_public_stability <= 100),
|
||||
lover_role text NULL CHECK (lover_role IN ('secret_affair', 'lover', 'mistress_or_favorite')),
|
||||
affection integer NOT NULL DEFAULT 50 CHECK (affection >= 0 AND affection <= 100),
|
||||
visibility integer NOT NULL DEFAULT 15 CHECK (visibility >= 0 AND visibility <= 100),
|
||||
discretion integer NOT NULL DEFAULT 50 CHECK (discretion >= 0 AND discretion <= 100),
|
||||
maintenance_level integer NOT NULL DEFAULT 50 CHECK (maintenance_level >= 0 AND maintenance_level <= 100),
|
||||
status_fit integer NOT NULL DEFAULT 0 CHECK (status_fit >= -2 AND status_fit <= 2),
|
||||
monthly_base_cost integer NOT NULL DEFAULT 0 CHECK (monthly_base_cost >= 0),
|
||||
months_underfunded integer NOT NULL DEFAULT 0 CHECK (months_underfunded >= 0),
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
acknowledged boolean NOT NULL DEFAULT false,
|
||||
exclusive_flag boolean NOT NULL DEFAULT false,
|
||||
last_monthly_processed_at timestamp with time zone NULL,
|
||||
last_daily_processed_at timestamp with time zone NULL,
|
||||
notes_json jsonb NULL,
|
||||
flags_json jsonb NULL,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT relationship_state_relationship_fk
|
||||
FOREIGN KEY (relationship_id)
|
||||
REFERENCES falukant_data.relationship(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS relationship_state_active_idx
|
||||
ON falukant_data.relationship_state (active);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
CREATE INDEX IF NOT EXISTS relationship_state_lover_role_idx
|
||||
ON falukant_data.relationship_state (lover_role);
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS legitimacy text NOT NULL DEFAULT 'legitimate';
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS birth_context text NOT NULL DEFAULT 'marriage';
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD COLUMN IF NOT EXISTS public_known boolean NOT NULL DEFAULT false;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'child_relation_legitimacy_chk'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD CONSTRAINT child_relation_legitimacy_chk
|
||||
CHECK (legitimacy IN ('legitimate', 'acknowledged_bastard', 'hidden_bastard'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'child_relation_birth_context_chk'
|
||||
) THEN
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
ADD CONSTRAINT child_relation_birth_context_chk
|
||||
CHECK (birth_context IN ('marriage', 'lover'));
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP CONSTRAINT IF EXISTS child_relation_birth_context_chk;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP CONSTRAINT IF EXISTS child_relation_legitimacy_chk;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP COLUMN IF EXISTS public_known;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP COLUMN IF EXISTS birth_context;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.child_relation
|
||||
DROP COLUMN IF EXISTS legitimacy;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
DROP TABLE IF EXISTS falukant_data.relationship_state;
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
INSERT INTO falukant_data.relationship_state (
|
||||
relationship_id,
|
||||
marriage_satisfaction,
|
||||
marriage_public_stability,
|
||||
lover_role,
|
||||
affection,
|
||||
visibility,
|
||||
discretion,
|
||||
maintenance_level,
|
||||
status_fit,
|
||||
monthly_base_cost,
|
||||
months_underfunded,
|
||||
active,
|
||||
acknowledged,
|
||||
exclusive_flag,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
r.id,
|
||||
55,
|
||||
55,
|
||||
CASE WHEN rt.tr = 'lover' THEN 'lover' ELSE NULL END,
|
||||
50,
|
||||
CASE WHEN rt.tr = 'lover' THEN 20 ELSE 15 END,
|
||||
CASE WHEN rt.tr = 'lover' THEN 45 ELSE 50 END,
|
||||
50,
|
||||
0,
|
||||
CASE WHEN rt.tr = 'lover' THEN 30 ELSE 0 END,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM falukant_data.relationship r
|
||||
INNER JOIN falukant_type.relationship rt
|
||||
ON rt.id = r.relationship_type_id
|
||||
LEFT JOIN falukant_data.relationship_state rs
|
||||
ON rs.relationship_id = r.id
|
||||
WHERE rs.id IS NULL
|
||||
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
DELETE FROM falukant_data.relationship_state rs
|
||||
USING falukant_data.relationship r
|
||||
INNER JOIN falukant_type.relationship rt
|
||||
ON rt.id = r.relationship_type_id
|
||||
WHERE rs.relationship_id = r.id
|
||||
AND rt.tr IN ('lover', 'wooing', 'engaged', 'married');
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'servant_count',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'servant_quality',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'servant_pay_level',
|
||||
{
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'normal'
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_order',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'household_order');
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_pay_level');
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_quality');
|
||||
await queryInterface.removeColumn({ schema: 'falukant_data', tableName: 'user_house' }, 'servant_count');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_score',
|
||||
{
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 10
|
||||
}
|
||||
);
|
||||
|
||||
await queryInterface.addColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_reasons_json',
|
||||
{
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_reasons_json'
|
||||
);
|
||||
await queryInterface.removeColumn(
|
||||
{ schema: 'falukant_data', tableName: 'user_house' },
|
||||
'household_tension_score'
|
||||
);
|
||||
}
|
||||
};
|
||||
83
backend/migrations/20260323010000-expand-debtors-prism.cjs
Normal file
83
backend/migrations/20260323010000-expand-debtors-prism.cjs
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
||||
|
||||
await queryInterface.addColumn(table, 'status', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'delinquent'
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'entered_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'released_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'debt_at_entry', {
|
||||
type: Sequelize.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'remaining_debt', {
|
||||
type: Sequelize.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'days_overdue', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'reason', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'creditworthiness_penalty', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'next_forced_action', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'assets_seized_json', {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true
|
||||
});
|
||||
|
||||
await queryInterface.addColumn(table, 'public_known', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
const table = { schema: 'falukant_data', tableName: 'debtors_prism' };
|
||||
|
||||
await queryInterface.removeColumn(table, 'public_known');
|
||||
await queryInterface.removeColumn(table, 'assets_seized_json');
|
||||
await queryInterface.removeColumn(table, 'next_forced_action');
|
||||
await queryInterface.removeColumn(table, 'creditworthiness_penalty');
|
||||
await queryInterface.removeColumn(table, 'reason');
|
||||
await queryInterface.removeColumn(table, 'days_overdue');
|
||||
await queryInterface.removeColumn(table, 'remaining_debt');
|
||||
await queryInterface.removeColumn(table, 'debt_at_entry');
|
||||
await queryInterface.removeColumn(table, 'released_at');
|
||||
await queryInterface.removeColumn(table, 'entered_at');
|
||||
await queryInterface.removeColumn(table, 'status');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.transport
|
||||
ADD COLUMN IF NOT EXISTS guard_count integer NOT NULL DEFAULT 0;
|
||||
`);
|
||||
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.underground
|
||||
ALTER COLUMN victim_id DROP NOT NULL;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE falukant_data.transport
|
||||
DROP COLUMN IF EXISTS guard_count;
|
||||
`);
|
||||
}
|
||||
};
|
||||
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';
|
||||
@@ -66,6 +68,7 @@ import PromotionalGiftCharacterTrait from './falukant/predefine/promotional_gift
|
||||
import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js';
|
||||
import RelationshipType from './falukant/type/relationship.js';
|
||||
import Relationship from './falukant/data/relationship.js';
|
||||
import RelationshipState from './falukant/data/relationship_state.js';
|
||||
import PromotionalGiftLog from './falukant/log/promotional_gift.js';
|
||||
import HouseType from './falukant/type/house.js';
|
||||
import BuyableHouse from './falukant/data/buyable_house.js';
|
||||
@@ -93,6 +96,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 +109,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 +171,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 +354,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 +426,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' });
|
||||
|
||||
@@ -433,6 +461,8 @@ export default function setupAssociations() {
|
||||
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', });
|
||||
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
|
||||
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', });
|
||||
Relationship.hasOne(RelationshipState, { foreignKey: 'relationshipId', as: 'state' });
|
||||
RelationshipState.belongsTo(Relationship, { foreignKey: 'relationshipId', as: 'relationship' });
|
||||
|
||||
PromotionalGiftLog.belongsTo(PromotionalGift, { foreignKey: 'giftId', as: 'gift' });
|
||||
PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' });
|
||||
@@ -558,14 +588,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 +889,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 +1061,40 @@ 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;
|
||||
@@ -34,6 +34,18 @@ FalukantCharacter.init(
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1}
|
||||
,
|
||||
reputation: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
// Initialisierung: zufällig 20..80 (Prozent)
|
||||
// DB-seitig per DEFAULT umgesetzt, damit es auch ohne App-Logic gilt.
|
||||
defaultValue: sequelize.literal('(floor(random()*61)+20)'),
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
|
||||
@@ -27,7 +27,25 @@ ChildRelation.init(
|
||||
isHeir: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: true,
|
||||
default: false}
|
||||
default: false},
|
||||
legitimacy: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'legitimate',
|
||||
validate: {
|
||||
isIn: [['legitimate', 'acknowledged_bastard', 'hidden_bastard']]
|
||||
}},
|
||||
birthContext: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'marriage',
|
||||
validate: {
|
||||
isIn: [['marriage', 'lover']]
|
||||
}},
|
||||
publicKnown: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
|
||||
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;
|
||||
@@ -7,7 +7,57 @@ DebtorsPrism.init({
|
||||
// Verknüpfung auf FalukantCharacter
|
||||
characterId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false}}, {
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'delinquent'
|
||||
},
|
||||
enteredAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
releasedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
debtAtEntry: {
|
||||
type: DataTypes.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
},
|
||||
remainingDebt: {
|
||||
type: DataTypes.DECIMAL(14, 2),
|
||||
allowNull: true
|
||||
},
|
||||
daysOverdue: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
reason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
creditworthinessPenalty: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
nextForcedAction: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
assetsSeizedJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
publicKnown: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'DebtorsPrism',
|
||||
tableName: 'debtors_prism',
|
||||
|
||||
@@ -29,6 +29,10 @@ Director.init({
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true},
|
||||
mayRepairVehicles: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true},
|
||||
lastSalaryPayout: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
|
||||
@@ -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,
|
||||
|
||||
141
backend/models/falukant/data/relationship_state.js
Normal file
141
backend/models/falukant/data/relationship_state.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class RelationshipState extends Model {}
|
||||
|
||||
RelationshipState.init(
|
||||
{
|
||||
relationshipId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
marriageSatisfaction: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
marriagePublicStability: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
loverRole: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isIn: [[null, 'secret_affair', 'lover', 'mistress_or_favorite'].filter(Boolean)],
|
||||
},
|
||||
},
|
||||
affection: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 15,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
discretion: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
maintenanceLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
statusFit: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
validate: {
|
||||
min: -2,
|
||||
max: 2,
|
||||
},
|
||||
},
|
||||
monthlyBaseCost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
validate: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
monthsUnderfunded: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
validate: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
acknowledged: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
exclusiveFlag: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
lastMonthlyProcessedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
lastDailyProcessedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
notesJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
},
|
||||
flagsJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'RelationshipState',
|
||||
tableName: 'relationship_state',
|
||||
schema: 'falukant_data',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default RelationshipState;
|
||||
@@ -6,7 +6,8 @@ class FalukantStock extends Model { }
|
||||
FalukantStock.init({
|
||||
branchId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
stockTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
@@ -25,6 +25,11 @@ Transport.init(
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
guardCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
@@ -38,4 +43,3 @@ Transport.init(
|
||||
|
||||
export default Transport;
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Underground.init({
|
||||
allowNull: false},
|
||||
victimId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false},
|
||||
allowNull: true},
|
||||
parameters: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true},
|
||||
|
||||
@@ -24,6 +24,35 @@ UserHouse.init({
|
||||
allowNull: false,
|
||||
defaultValue: 100
|
||||
},
|
||||
servantCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
servantQuality: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 50
|
||||
},
|
||||
servantPayLevel: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: 'normal'
|
||||
},
|
||||
householdOrder: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 55
|
||||
},
|
||||
householdTensionScore: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 10
|
||||
},
|
||||
householdTensionReasonsJson: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
},
|
||||
houseTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
|
||||
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;
|
||||
59
backend/models/falukant/log/reputation_action.js
Normal file
59
backend/models/falukant/log/reputation_action.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class ReputationActionLog extends Model {}
|
||||
|
||||
ReputationActionLog.init(
|
||||
{
|
||||
falukantUserId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'falukant_user_id',
|
||||
},
|
||||
actionTypeId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'action_type_id',
|
||||
},
|
||||
cost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
baseGain: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'base_gain',
|
||||
},
|
||||
gain: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
timesUsedBefore: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'times_used_before',
|
||||
},
|
||||
actionTimestamp: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
field: 'action_timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'ReputationActionLog',
|
||||
tableName: 'reputation_action',
|
||||
schema: 'falukant_log',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['falukant_user_id', 'action_type_id'] },
|
||||
{ fields: ['action_timestamp'] },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
export default ReputationActionLog;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
51
backend/models/falukant/type/reputation_action.js
Normal file
51
backend/models/falukant/type/reputation_action.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../../utils/sequelize.js';
|
||||
|
||||
class ReputationActionType extends Model {}
|
||||
|
||||
ReputationActionType.init(
|
||||
{
|
||||
tr: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
cost: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
baseGain: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'base_gain',
|
||||
},
|
||||
decayFactor: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false,
|
||||
field: 'decay_factor',
|
||||
},
|
||||
minGain: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'min_gain',
|
||||
},
|
||||
decayWindowDays: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 7,
|
||||
field: 'decay_window_days',
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'ReputationActionType',
|
||||
tableName: 'reputation_action',
|
||||
schema: 'falukant_type',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default ReputationActionType;
|
||||
|
||||
|
||||
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';
|
||||
@@ -64,6 +67,7 @@ import Notification from './falukant/log/notification.js';
|
||||
import MarriageProposal from './falukant/data/marriage_proposal.js';
|
||||
import RelationshipType from './falukant/type/relationship.js';
|
||||
import Relationship from './falukant/data/relationship.js';
|
||||
import RelationshipState from './falukant/data/relationship_state.js';
|
||||
import CharacterTrait from './falukant/type/character_trait.js';
|
||||
import FalukantCharacterTrait from './falukant/data/falukant_character_trait.js';
|
||||
import Mood from './falukant/type/mood.js';
|
||||
@@ -79,12 +83,15 @@ import Party from './falukant/data/party.js';
|
||||
import MusicType from './falukant/type/music.js';
|
||||
import BanquetteType from './falukant/type/banquette.js';
|
||||
import PartyInvitedNobility from './falukant/data/partyInvitedNobility.js';
|
||||
import ReputationActionType from './falukant/type/reputation_action.js';
|
||||
import ReputationActionLog from './falukant/log/reputation_action.js';
|
||||
import ChildRelation from './falukant/data/child_relation.js';
|
||||
import LearnRecipient from './falukant/type/learn_recipient.js';
|
||||
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';
|
||||
@@ -111,6 +118,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';
|
||||
@@ -127,13 +141,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,
|
||||
@@ -177,6 +203,7 @@ const models = {
|
||||
ProductType,
|
||||
Knowledge,
|
||||
TitleOfNobility,
|
||||
TitleBenefit,
|
||||
TitleRequirement,
|
||||
BranchType,
|
||||
Branch,
|
||||
@@ -193,6 +220,7 @@ const models = {
|
||||
MarriageProposal,
|
||||
RelationshipType,
|
||||
Relationship,
|
||||
RelationshipState,
|
||||
CharacterTrait,
|
||||
FalukantCharacterTrait,
|
||||
Mood,
|
||||
@@ -208,12 +236,15 @@ const models = {
|
||||
BanquetteType,
|
||||
Party,
|
||||
PartyInvitedNobility,
|
||||
ReputationActionType,
|
||||
ReputationActionLog,
|
||||
ChildRelation,
|
||||
LearnRecipient,
|
||||
Learning,
|
||||
Credit,
|
||||
DebtorsPrism,
|
||||
HealthActivity,
|
||||
ProductPriceHistory,
|
||||
RegionDistance,
|
||||
VehicleType,
|
||||
Vehicle,
|
||||
@@ -229,6 +260,11 @@ const models = {
|
||||
ElectionResult,
|
||||
PoliticalOfficeHistory,
|
||||
ElectionHistory,
|
||||
RelationshipChangeLog,
|
||||
ChurchOfficeType,
|
||||
ChurchOfficeRequirement,
|
||||
ChurchOffice,
|
||||
ChurchApplication,
|
||||
UndergroundType,
|
||||
Underground,
|
||||
WeatherType,
|
||||
@@ -259,6 +295,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;
|
||||
4264
backend/package-lock.json
generated
4264
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,37 +9,41 @@
|
||||
"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": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"amqplib": "^0.10.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-redis": "^7.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"amqplib": "^0.10.9",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"connect-redis": "^9.0.0",
|
||||
"cors": "^2.8.6",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.1.7",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.1",
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"multer": "^2.0.0",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^7.0.11",
|
||||
"pg": "^8.12.0",
|
||||
"dompurify": "^3.3.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.19.0",
|
||||
"i18n": "^0.15.3",
|
||||
"joi": "^18.0.2",
|
||||
"jsdom": "^29.0.1",
|
||||
"multer": "^2.1.1",
|
||||
"mysql2": "^3.20.0",
|
||||
"nodemailer": "^8.0.3",
|
||||
"pg": "^8.20.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"redis": "^4.7.0",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.34.3",
|
||||
"socket.io": "^4.7.5",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.0"
|
||||
"redis": "^5.11.0",
|
||||
"sequelize": "^6.37.8",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io": "^4.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.20.0",
|
||||
"@gltf-transform/cli": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sequelize-cli": "^6.6.2"
|
||||
"sequelize-cli": "^6.6.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ router.put('/falukant/regions/:id/map', authenticate, adminController.updateFalu
|
||||
router.get('/falukant/region-distances', authenticate, adminController.getRegionDistances);
|
||||
router.post('/falukant/region-distances', authenticate, adminController.upsertRegionDistance);
|
||||
router.delete('/falukant/region-distances/:id', authenticate, adminController.deleteRegionDistance);
|
||||
router.post('/falukant/npcs/create', authenticate, adminController.createNPCs);
|
||||
router.get('/falukant/npcs/status/:jobId', authenticate, adminController.getNPCsCreationStatus);
|
||||
router.get('/falukant/titles', authenticate, adminController.getTitlesOfNobility);
|
||||
|
||||
// --- Minigames Admin ---
|
||||
router.get('/minigames/match3/campaigns', authenticate, adminController.getMatch3Campaigns);
|
||||
|
||||
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,24 +39,50 @@ 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.post('/family/lover', falukantController.createLoverRelationship);
|
||||
router.post('/family/marriage/spend-time', falukantController.spendTimeWithSpouse);
|
||||
router.post('/family/marriage/gift', falukantController.giftToSpouse);
|
||||
router.post('/family/marriage/reconcile', falukantController.reconcileMarriage);
|
||||
router.post('/family/lover/:relationshipId/maintenance', falukantController.setLoverMaintenance);
|
||||
router.post('/family/lover/:relationshipId/acknowledge', falukantController.acknowledgeLover);
|
||||
router.post('/family/lover/:relationshipId/end', falukantController.endLoverRelationship);
|
||||
router.get('/heirs/potential', falukantController.getPotentialHeirs);
|
||||
router.post('/heirs/select', falukantController.selectHeir);
|
||||
router.get('/family/gifts', falukantController.getGifts);
|
||||
router.get('/family/children', falukantController.getChildren);
|
||||
router.post('/family/gift', falukantController.sendGift);
|
||||
router.get('/family', falukantController.getFamily);
|
||||
router.get('/nobility/titels', falukantController.getTitlesOfNobility);
|
||||
router.get('/reputation/actions', falukantController.getReputationActions);
|
||||
router.post('/reputation/actions', falukantController.executeReputationAction);
|
||||
router.get('/houses/types', falukantController.getHouseTypes);
|
||||
router.get('/houses/buyable', falukantController.getBuyableHouses);
|
||||
router.get('/houses', falukantController.getUserHouse);
|
||||
router.post('/houses/renovate-all', falukantController.renovateAll);
|
||||
router.post('/houses/renovate', falukantController.renovate);
|
||||
router.post('/houses/servants/hire', falukantController.hireServants);
|
||||
router.post('/houses/servants/dismiss', falukantController.dismissServants);
|
||||
router.post('/houses/servants/pay-level', falukantController.setServantPayLevel);
|
||||
router.post('/houses/order', falukantController.tidyHousehold);
|
||||
router.post('/houses', falukantController.buyUserHouse);
|
||||
router.get('/party/types', falukantController.getPartyTypes);
|
||||
router.post('/party', falukantController.createParty);
|
||||
router.get('/party', falukantController.getParties);
|
||||
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);
|
||||
@@ -66,13 +94,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);
|
||||
@@ -83,6 +112,8 @@ router.post('/transports', falukantController.createTransport);
|
||||
router.get('/transports/route', falukantController.getTransportRoute);
|
||||
router.get('/transports/branch/:branchId', falukantController.getBranchTransports);
|
||||
router.get('/underground/types', falukantController.getUndergroundTypes);
|
||||
router.get('/underground/raid-regions', falukantController.getRaidTransportRegions);
|
||||
router.get('/underground/activities', falukantController.getUndergroundActivities);
|
||||
router.get('/notifications', falukantController.getNotifications);
|
||||
router.get('/notifications/all', falukantController.getAllNotifications);
|
||||
router.post('/notifications/mark-shown', falukantController.markNotificationsShown);
|
||||
|
||||
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;
|
||||
61
backend/routers/vocabRouter.js
Normal file
61
backend/routers/vocabRouter.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import VocabController from '../controllers/vocabController.js';
|
||||
|
||||
const router = express.Router();
|
||||
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);
|
||||
|
||||
// Kapitel
|
||||
router.get('/languages/:languageId/chapters', vocabController.listChapters);
|
||||
router.post('/languages/:languageId/chapters', vocabController.createChapter);
|
||||
router.get('/languages/:languageId/vocabs', vocabController.listLanguageVocabs);
|
||||
router.get('/languages/:languageId/search', vocabController.searchVocabs);
|
||||
|
||||
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);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user