5 Commits

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

0
.codex
View File

View File

@@ -1,9 +0,0 @@
---
description: C++-Worker unter src/ sind obsolet — nicht erweitern oder als Quelle für Spiellogik nutzen
alwaysApply: true
---
# Legacy C++ (`src/`)
- Verzeichnis **`src/`** (C++-Worker, WebSocket-Server): **obsolet**. Keine neuen Features, keine fachlichen Fixes dort planen oder umsetzen, sofern der Nutzer nicht ausdrücklich etwas anderes verlangt.
- Falukant-Hintergrundlogik: **Backend** (`backend/`), **externer Daemon**, **Frontend** — siehe `docs/LEGACY_CPP_WORKERS.md`.

View File

@@ -1,121 +0,0 @@
name: Deploy to production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect vocab course changes
id: vocab_course_changes
shell: bash
run: |
set -euo pipefail
BASE="${{ gitea.event.before }}"
HEAD="${{ gitea.sha }}"
if [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
BASE="HEAD~1"
fi
git diff --name-only "$BASE" "$HEAD" > changed-files.txt
cat changed-files.txt
COMMIT_MESSAGE="$(git log -1 --pretty=%B "$HEAD" || true)"
if echo "$COMMIT_MESSAGE" | grep -qi '\[force-deploy\]'; then
echo "force_deploy=true" >> "$GITHUB_OUTPUT"
else
echo "force_deploy=false" >> "$GITHUB_OUTPUT"
fi
if grep -E '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))' changed-files.txt; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
if grep -E '^frontend/' changed-files.txt >/dev/null; then
echo "frontend_changed=true" >> "$GITHUB_OUTPUT"
else
echo "frontend_changed=false" >> "$GITHUB_OUTPUT"
fi
if grep -E '^backend/' changed-files.txt \
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab)$' >/dev/null; then
echo "backend_app_changed=true" >> "$GITHUB_OUTPUT"
else
echo "backend_app_changed=false" >> "$GITHUB_OUTPUT"
fi
# App-Code-Änderungen, die einen echten Deploy benötigen
# (Frontend oder Backend außerhalb reiner Kurs-/Dokument-Sync-Dateien)
if grep -E '^(frontend/|backend/)' changed-files.txt \
| grep -Ev '^(backend/scripts/.*(bisaya|course|didactics|vocab)|backend/sql/.*vocab|backend/(migrations-active|migrations-archive)/.*vocab|docs/.*(COURSE|VOCAB|BISAYA|GERMAN_FOR_BISAYA))'; then
echo "app_changed=true" >> "$GITHUB_OUTPUT"
else
echo "app_changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Prepare SSH
run: |
mkdir -p ~/.ssh
printf "%s" "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "${{ secrets.PROD_PORT }}" "${{ secrets.PROD_HOST }}" >> ~/.ssh/known_hosts
- name: Test SSH connection
run: |
ssh -i ~/.ssh/id_ed25519 \
-o StrictHostKeyChecking=no \
-o BatchMode=yes \
-p "${{ secrets.PROD_PORT }}" \
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
"echo SSH OK"
- name: Run deployment script
if: steps.vocab_course_changes.outputs.app_changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
run: |
DEPLOY_FLAGS=""
if [ "${{ steps.vocab_course_changes.outputs.force_deploy }}" = "true" ]; then
DEPLOY_FLAGS=""
elif [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" != "true" ]; then
DEPLOY_FLAGS="--skip-frontend"
elif [ "${{ steps.vocab_course_changes.outputs.frontend_changed }}" = "true" ] && [ "${{ steps.vocab_course_changes.outputs.backend_app_changed }}" != "true" ]; then
DEPLOY_FLAGS="--skip-backend"
fi
DEPLOY_TARGET="${{ secrets.PROD_DEPLOY_TARGET }}"
if [ -z "$DEPLOY_TARGET" ]; then
DEPLOY_TARGET="/opt/yourpart-green"
fi
echo "Deploy-Flags: ${DEPLOY_FLAGS:-<none>}"
echo "Deploy-Target: $DEPLOY_TARGET"
ssh -i ~/.ssh/id_ed25519 \
-p "${{ secrets.PROD_PORT }}" \
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
"/home/tsschulz/deploy-yourpart-bluegreen.sh ${DEPLOY_TARGET} ${DEPLOY_FLAGS}"
- name: Skip full deployment (no app changes)
if: steps.vocab_course_changes.outputs.app_changed != 'true' && steps.vocab_course_changes.outputs.force_deploy != 'true'
run: |
echo "Kein Full-Deploy: Es wurden keine Frontend/Backend-App-Dateien geändert."
- name: Sync vocab course content
if: steps.vocab_course_changes.outputs.changed == 'true' || steps.vocab_course_changes.outputs.force_deploy == 'true'
run: |
ssh -i ~/.ssh/id_ed25519 \
-p "${{ secrets.PROD_PORT }}" \
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
"cd /opt/yourpart && npm --prefix backend run sync:vocab-courses"

11
.gitignore vendored
View File

@@ -5,9 +5,7 @@
.depbe.sh .depbe.sh
node_modules node_modules
node_modules/* node_modules/*
# package-lock.json wird versioniert (npm ci im Deploy braucht konsistente Locks zu package.json)
backend/.env backend/.env
backend/.env.local
backend/images backend/images
backend/images/* backend/images/*
backend/node_modules backend/node_modules
@@ -17,14 +15,5 @@ frontend/node_modules
frontend/node_modules/* frontend/node_modules/*
frontend/dist frontend/dist
frontend/dist/* frontend/dist/*
frontend/scripts/.i18n-de-fr-cache.json
frontend/scripts/.falukant-fr-smooth-cache.json
frontend/ceb-locale-audit-report.json
frontedtree.txt frontedtree.txt
backend/dist/ backend/dist/
backend/data/model-cache
build
build/*
.vscode
.vscode/*
.clang-format

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
## zum testen des push
Hinweis: Das Verzeichnis **`src/`** (C++-Worker) ist veraltet; siehe [`docs/LEGACY_CPP_WORKERS.md`](docs/LEGACY_CPP_WORKERS.md).

View File

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

View File

@@ -3,7 +3,7 @@
This project now supports a per-region sales tax (`tax_percent`) for Falukant. This project now supports a per-region sales tax (`tax_percent`) for Falukant.
Migration Migration
- A SQL migration was added: `backend/migrations-archive/20260101000000-add-tax-percent-to-region.cjs`. - A SQL migration was added: `backend/migrations/20260101000000-add-tax-percent-to-region.cjs`.
- It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`. - It adds `tax_percent` numeric NOT NULL DEFAULT 7 to `falukant_data.region`.
Runtime configuration Runtime configuration

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,90 +7,40 @@ import fs from 'fs';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const quietEnv = process.env.QUIET_ENV_LOGS === '1';
const dotenvQuiet = quietEnv || process.env.DOTENV_CONFIG_QUIET === '1';
function log(...args) {
if (!quietEnv) console.log(...args);
}
function warn(...args) {
console.warn(...args);
}
// Versuche zuerst Produktions-.env, dann lokale .env // Versuche zuerst Produktions-.env, dann lokale .env
const productionEnvPath = '/opt/yourpart/backend/.env'; const productionEnvPath = '/opt/yourpart/backend/.env';
const localEnvPath = path.resolve(__dirname, '../.env'); const localEnvPath = path.resolve(__dirname, '../.env');
let envPath = localEnvPath; // Fallback let envPath = localEnvPath; // Fallback
let usingProduction = false;
if (fs.existsSync(productionEnvPath)) { if (fs.existsSync(productionEnvPath)) {
// Prüfe Lesbarkeit bevor wir versuchen, sie zu laden
try {
fs.accessSync(productionEnvPath, fs.constants.R_OK);
envPath = productionEnvPath; envPath = productionEnvPath;
usingProduction = true; console.log('[env] Lade Produktions-.env:', productionEnvPath);
log('[env] Produktions-.env gefunden und lesbar:', productionEnvPath);
} catch (err) {
if (!quietEnv) {
warn('[env] Produktions-.env vorhanden, aber nicht lesbar - verwende lokale .env stattdessen:', productionEnvPath);
warn('[env] Fehler:', err && err.message);
}
envPath = localEnvPath;
}
} else { } else {
log('[env] Produktions-.env nicht gefunden, lade lokale .env:', localEnvPath); console.log('[env] Lade lokale .env:', localEnvPath);
} }
// Lade .env-Datei (robust gegen Fehler) // Lade .env-Datei
log('[env] Versuche .env zu laden von:', envPath); console.log('[env] Versuche .env zu laden von:', envPath);
log('[env] Datei existiert:', fs.existsSync(envPath)); console.log('[env] Datei existiert:', fs.existsSync(envPath));
let result; console.log('[env] Datei lesbar:', fs.accessSync ? (() => { try { fs.accessSync(envPath, fs.constants.R_OK); return true; } catch { return false; } })() : 'unbekannt');
try {
result = dotenv.config({ path: envPath, quiet: dotenvQuiet }); const result = dotenv.config({ path: envPath });
if (result.error) { if (result.error) {
warn('[env] Konnte .env nicht laden:', result.error.message); console.warn('[env] Konnte .env nicht laden:', result.error.message);
warn('[env] Fehler-Details:', result.error); console.warn('[env] Fehler-Details:', result.error);
} else { } else {
log('[env] .env erfolgreich geladen von:', envPath, usingProduction ? '(production)' : '(local)'); console.log('[env] .env erfolgreich geladen von:', envPath);
log('[env] Geladene Variablen:', Object.keys(result.parsed || {})); console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
}
} catch (err) {
// Sollte nicht passieren, aber falls dotenv intern eine Exception wirft (z.B. EACCES), fange sie ab
warn('[env] Unerwarteter Fehler beim Laden der .env:', err && err.message);
warn('[env] Stack:', err && err.stack);
if (envPath !== localEnvPath && fs.existsSync(localEnvPath)) {
log('[env] Versuche stattdessen lokale .env:', localEnvPath);
try {
result = dotenv.config({ path: localEnvPath, quiet: dotenvQuiet });
if (!result.error) {
log('[env] Lokale .env erfolgreich geladen von:', localEnvPath);
}
} catch (err2) {
warn('[env] Konnte lokale .env auch nicht laden:', err2 && err2.message);
}
}
} }
// Lokale Überschreibungen (nicht committen): z. B. SSH-Tunnel DB_HOST=127.0.0.1 DB_PORT=60000 // Debug: Zeige Redis-Konfiguration
const localOverridePath = path.resolve(__dirname, '../.env.local');
if (fs.existsSync(localOverridePath)) {
const overrideResult = dotenv.config({ path: localOverridePath, override: true, quiet: dotenvQuiet });
if (!overrideResult.error) {
log('[env] .env.local geladen (überschreibt Werte, z. B. SSH-Tunnel)');
} else {
warn('[env] .env.local vorhanden, aber Laden fehlgeschlagen:', overrideResult.error?.message);
}
}
if (!quietEnv) {
console.log('[env] Redis-Konfiguration:'); console.log('[env] Redis-Konfiguration:');
console.log('[env] REDIS_HOST:', process.env.REDIS_HOST); console.log('[env] REDIS_HOST:', process.env.REDIS_HOST);
console.log('[env] REDIS_PORT:', process.env.REDIS_PORT); console.log('[env] REDIS_PORT:', process.env.REDIS_PORT);
console.log('[env] REDIS_PASSWORD:', process.env.REDIS_PASSWORD ? '***gesetzt***' : 'NICHT GESETZT'); console.log('[env] REDIS_PASSWORD:', process.env.REDIS_PASSWORD ? '***gesetzt***' : 'NICHT GESETZT');
console.log('[env] REDIS_URL:', process.env.REDIS_URL); console.log('[env] REDIS_URL:', process.env.REDIS_URL);
}
if (!process.env.SECRET_KEY) { if (!process.env.SECRET_KEY) {
warn('[env] SECRET_KEY nicht gesetzt in .env'); console.warn('[env] SECRET_KEY nicht gesetzt in .env');
} }
export {}; export {};

View File

@@ -1,33 +0,0 @@
const path = require('path');
const dotenv = require('dotenv');
const envPath = process.env.SEQUELIZE_ENV_FILE
? path.resolve(process.cwd(), process.env.SEQUELIZE_ENV_FILE)
: path.resolve(process.cwd(), '.env');
dotenv.config({ path: envPath });
const dialectOptions = {};
if (process.env.DB_SSL === '1' || process.env.PGSSLMODE === 'require') {
dialectOptions.ssl = process.env.DB_SSL_REJECT_UNAUTHORIZED === '0'
? { rejectUnauthorized: false }
: true;
}
// pg/SCRAM: password muss ein String sein; bei fehlender .env sonst undefined-Fallen vermeiden
const shared = {
username: process.env.DB_USER != null ? String(process.env.DB_USER) : undefined,
password: process.env.DB_PASS != null ? String(process.env.DB_PASS) : '',
database: process.env.DB_NAME != null ? String(process.env.DB_NAME) : undefined,
host: process.env.DB_HOST || '127.0.0.1',
port: Number.parseInt(process.env.DB_PORT || '5432', 10),
dialect: 'postgres',
logging: false,
dialectOptions
};
module.exports = {
development: shared,
test: shared,
production: shared
};

View File

@@ -13,11 +13,6 @@ class AdminController {
this.searchUser = this.searchUser.bind(this); this.searchUser = this.searchUser.bind(this);
this.getFalukantUserById = this.getFalukantUserById.bind(this); this.getFalukantUserById = this.getFalukantUserById.bind(this);
this.changeFalukantUser = this.changeFalukantUser.bind(this); this.changeFalukantUser = this.changeFalukantUser.bind(this);
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this);
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this); this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
this.updateFalukantStock = this.updateFalukantStock.bind(this); this.updateFalukantStock = this.updateFalukantStock.bind(this);
this.addFalukantStock = this.addFalukantStock.bind(this); this.addFalukantStock = this.addFalukantStock.bind(this);
@@ -34,16 +29,6 @@ class AdminController {
this.getUser = this.getUser.bind(this); this.getUser = this.getUser.bind(this);
this.getUsers = this.getUsers.bind(this); this.getUsers = this.getUsers.bind(this);
this.updateUser = this.updateUser.bind(this); this.updateUser = this.updateUser.bind(this);
this.resetUserVocabLessonProgress = this.resetUserVocabLessonProgress.bind(this);
this.markUserVocabLessonsCompleteThrough = this.markUserVocabLessonsCompleteThrough.bind(this);
this.getUserVocabCourses = this.getUserVocabCourses.bind(this);
this.getVocabCourseForAdmin = this.getVocabCourseForAdmin.bind(this);
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
this.setAdultVerificationStatus = this.setAdultVerificationStatus.bind(this);
this.getAdultVerificationDocument = this.getAdultVerificationDocument.bind(this);
this.getEroticModerationReports = this.getEroticModerationReports.bind(this);
this.applyEroticModerationAction = this.applyEroticModerationAction.bind(this);
this.getEroticModerationPreview = this.getEroticModerationPreview.bind(this);
// Rights // Rights
this.listRightTypes = this.listRightTypes.bind(this); this.listRightTypes = this.listRightTypes.bind(this);
@@ -54,9 +39,6 @@ class AdminController {
// Statistics // Statistics
this.getUserStatistics = this.getUserStatistics.bind(this); this.getUserStatistics = this.getUserStatistics.bind(this);
this.getFalukantRegions = this.getFalukantRegions.bind(this); this.getFalukantRegions = this.getFalukantRegions.bind(this);
this.getFalukantAllRegions = this.getFalukantAllRegions.bind(this);
this.getFalukantRegionTypes = this.getFalukantRegionTypes.bind(this);
this.createFalukantRegion = this.createFalukantRegion.bind(this);
this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this); this.updateFalukantRegionMap = this.updateFalukantRegionMap.bind(this);
this.getRegionDistances = this.getRegionDistances.bind(this); this.getRegionDistances = this.getRegionDistances.bind(this);
this.upsertRegionDistance = this.upsertRegionDistance.bind(this); this.upsertRegionDistance = this.upsertRegionDistance.bind(this);
@@ -137,168 +119,6 @@ class AdminController {
} }
} }
async resetUserVocabLessonProgress(req, res) {
const schema = Joi.object({
lessonId: Joi.number().integer().positive().required()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.adminResetUserVocabLessonProgress(requester, id, value.lessonId);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (err.message === 'lessonnotfound' ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
async markUserVocabLessonsCompleteThrough(req, res) {
const schema = Joi.object({
courseId: Joi.number().integer().positive().required(),
throughLessonNumber: Joi.number().integer().positive().required()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.adminMarkUserVocabLessonsCompleteThrough(
requester,
id,
value.courseId,
value.throughLessonNumber
);
res.status(200).json(result);
} catch (err) {
let status = 500;
if (err.message === 'noaccess') status = 403;
else if (err.message === 'notenrolled') status = 403;
else if (err.message === 'badrequest') status = 400;
res.status(status).json({ error: err.message });
}
}
async getUserVocabCourses(req, res) {
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.adminListUserEnrolledVocabCourses(requester, id);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (err.message === 'notfound' ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
async getVocabCourseForAdmin(req, res) {
try {
const { userid: requester } = req.headers;
const { courseId } = req.params;
const result = await AdminService.adminGetVocabCourseWithLessons(requester, courseId);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (err.message === 'coursenotfound' ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
async getAdultVerificationRequests(req, res) {
try {
const { userid: requester } = req.headers;
const { status = 'pending' } = req.query;
const result = await AdminService.getAdultVerificationRequests(requester, status);
res.status(200).json(result);
} catch (error) {
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async setAdultVerificationStatus(req, res) {
const schema = Joi.object({
status: Joi.string().valid('approved', 'rejected', 'pending').required()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.setAdultVerificationStatus(requester, id, value.status);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (['notfound', 'notadult', 'wrongstatus', 'missingparamtype'].includes(err.message) ? 400 : 500);
res.status(status).json({ error: err.message });
}
}
async getAdultVerificationDocument(req, res) {
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.getAdultVerificationDocument(requester, id);
res.setHeader('Content-Type', result.mimeType);
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(result.originalName)}"`);
res.sendFile(result.filePath);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (['notfound', 'norequest', 'nofile'].includes(err.message) ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
async getEroticModerationReports(req, res) {
try {
const { userid: requester } = req.headers;
const { status = 'open' } = req.query;
const result = await AdminService.getEroticModerationReports(requester, status);
res.status(200).json(result);
} catch (error) {
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async applyEroticModerationAction(req, res) {
const schema = Joi.object({
action: Joi.string().valid('dismiss', 'hide_content', 'restore_content', 'delete_content', 'block_uploads', 'revoke_access').required(),
note: Joi.string().allow('', null).max(2000).optional()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: requester } = req.headers;
const { id } = req.params;
const result = await AdminService.applyEroticModerationAction(requester, Number(id), value.action, value.note || null);
res.status(200).json(result);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (['notfound', 'targetnotfound', 'wrongaction'].includes(err.message) ? 400 : 500);
res.status(status).json({ error: err.message });
}
}
async getEroticModerationPreview(req, res) {
try {
const { userid: requester } = req.headers;
const { type, targetId } = req.params;
const result = await AdminService.getEroticModerationPreview(requester, type, Number(targetId));
res.setHeader('Content-Type', result.mimeType);
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(result.originalName)}"`);
res.sendFile(result.filePath);
} catch (err) {
const status = err.message === 'noaccess' ? 403 : (['notfound', 'nofile', 'wrongtype'].includes(err.message) ? 404 : 500);
res.status(status).json({ error: err.message });
}
}
// --- Rights --- // --- Rights ---
async listRightTypes(req, res) { async listRightTypes(req, res) {
try { try {
@@ -455,77 +275,6 @@ class AdminController {
} }
} }
async adminForceFalukantPregnancy(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId, fatherCharacterId, dueInDays } = req.body;
const response = await AdminService.adminForceFalukantPregnancy(userId, characterId, {
fatherCharacterId,
dueInDays,
});
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminGetPotentialFathersForCharacter(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.params;
const response = await AdminService.adminGetPotentialFathersForCharacter(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminClearFalukantPregnancy(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.body;
const response = await AdminService.adminClearFalukantPregnancy(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminForceFalukantBirth(req, res) {
try {
const { userid: userId } = req.headers;
const { motherCharacterId, fatherCharacterId, birthContext, legitimacy, gender } = req.body;
const response = await AdminService.adminForceFalukantBirth(userId, motherCharacterId, {
fatherCharacterId,
birthContext,
legitimacy,
gender,
});
res.status(200).json(response);
} catch (error) {
console.log(error);
res.status(400).json({ error: error.message });
}
}
async adminCleanupCharacterDeathArtifacts(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.params;
const response = await AdminService.adminCleanupCharacterDeathArtifacts(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess'
? 403
: (['invalidCharacter', 'notfound', 'targetnotdead'].includes(error.message) ? 400 : 500);
res.status(status).json({ error: error.message });
}
}
async getFalukantUserBranches(req, res) { async getFalukantUserBranches(req, res) {
try { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
@@ -586,42 +335,6 @@ class AdminController {
} }
} }
async getFalukantAllRegions(req, res) {
try {
const { userid: userId } = req.headers;
const regions = await AdminService.getFalukantAllRegions(userId);
res.status(200).json(regions);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async getFalukantRegionTypes(req, res) {
try {
const { userid: userId } = req.headers;
const types = await AdminService.getFalukantRegionTypes(userId);
res.status(200).json(types);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 500;
res.status(status).json({ error: error.message });
}
}
async createFalukantRegion(req, res) {
try {
const { userid: userId } = req.headers;
const created = await AdminService.createFalukantRegion(userId, req.body || {});
res.status(200).json(created);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess' ? 403 : 400;
res.status(status).json({ error: error.message });
}
}
async updateFalukantRegionMap(req, res) { async updateFalukantRegionMap(req, res) {
try { try {
const { userid: userId } = req.headers; const { userid: userId } = req.headers;
@@ -810,7 +523,6 @@ class AdminController {
title: Joi.string().min(1).max(255).required(), title: Joi.string().min(1).max(255).required(),
roomTypeId: Joi.number().integer().required(), roomTypeId: Joi.number().integer().required(),
isPublic: Joi.boolean().required(), isPublic: Joi.boolean().required(),
isAdultOnly: Joi.boolean().allow(null),
genderRestrictionId: Joi.number().integer().allow(null), genderRestrictionId: Joi.number().integer().allow(null),
minAge: Joi.number().integer().min(0).allow(null), minAge: Joi.number().integer().min(0).allow(null),
maxAge: Joi.number().integer().min(0).allow(null), maxAge: Joi.number().integer().min(0).allow(null),
@@ -822,7 +534,7 @@ class AdminController {
if (error) { if (error) {
return res.status(400).json({ error: error.details[0].message }); return res.status(400).json({ error: error.details[0].message });
} }
const room = await AdminService.updateRoom(userId, req.params.id, value); const room = await AdminService.updateRoom(req.params.id, value);
res.status(200).json(room); res.status(200).json(room);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -841,7 +553,6 @@ class AdminController {
title: Joi.string().min(1).max(255).required(), title: Joi.string().min(1).max(255).required(),
roomTypeId: Joi.number().integer().required(), roomTypeId: Joi.number().integer().required(),
isPublic: Joi.boolean().required(), isPublic: Joi.boolean().required(),
isAdultOnly: Joi.boolean().allow(null),
genderRestrictionId: Joi.number().integer().allow(null), genderRestrictionId: Joi.number().integer().allow(null),
minAge: Joi.number().integer().min(0).allow(null), minAge: Joi.number().integer().min(0).allow(null),
maxAge: Joi.number().integer().min(0).allow(null), maxAge: Joi.number().integer().min(0).allow(null),
@@ -868,7 +579,7 @@ class AdminController {
if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) { if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) {
return res.status(403).json({ error: 'Keine Berechtigung.' }); return res.status(403).json({ error: 'Keine Berechtigung.' });
} }
await AdminService.deleteRoom(userId, req.params.id); await AdminService.deleteRoom(req.params.id);
res.sendStatus(204); res.sendStatus(204);
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -29,8 +29,6 @@ class AuthController {
} catch (error) { } catch (error) {
if (error.message === 'credentialsinvalid') { if (error.message === 'credentialsinvalid') {
res.status(404).json({ error: error.message }); res.status(404).json({ error: error.message });
} else if (error.message === 'userblocked') {
res.status(403).json({ error: error.message });
} else { } else {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }

View File

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

View File

@@ -13,10 +13,6 @@ class ChatController {
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this); this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this); this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
this.getRoomList = this.getRoomList.bind(this); this.getRoomList = this.getRoomList.bind(this);
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
this.getOwnRooms = this.getOwnRooms.bind(this);
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
this.reportChatIncident = this.reportChatIncident.bind(this);
} }
async getMessages(req, res) { async getMessages(req, res) {
@@ -173,75 +169,12 @@ class ChatController {
async getRoomList(req, res) { async getRoomList(req, res) {
// Öffentliche Räume für Chat-Frontend // Öffentliche Räume für Chat-Frontend
try { try {
const { userid: hashedUserId } = req.headers; const rooms = await chatService.getRoomList();
const adultOnly = String(req.query.adultOnly || '').toLowerCase() === 'true';
const rooms = await chatService.getRoomList(hashedUserId, { adultOnly });
res.status(200).json(rooms); res.status(200).json(rooms);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
} }
async getRoomCreateOptions(req, res) {
try {
const options = await chatService.getRoomCreateOptions();
res.status(200).json(options);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getOwnRooms(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const rooms = await chatService.getOwnRooms(hashedUserId);
res.status(200).json(rooms);
} catch (error) {
const status = error.message === 'user_not_found' ? 404 : 500;
res.status(status).json({ error: error.message });
}
}
async deleteOwnRoom(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const roomId = Number.parseInt(req.params.id, 10);
if (!Number.isInteger(roomId) || roomId <= 0) {
return res.status(400).json({ error: 'invalid_room_id' });
}
await chatService.deleteOwnRoom(hashedUserId, roomId);
res.status(204).send();
} catch (error) {
const status = error.message === 'room_not_found_or_not_owner' || error.message === 'user_not_found' ? 404 : 500;
res.status(status).json({ error: error.message });
}
}
async reportChatIncident(req, res) {
const schema = Joi.object({
context: Joi.string().valid('random_chat', 'multi_chat', 'one_to_one').required(),
reporterHashedId: Joi.string().allow('', null),
reporterRandomId: Joi.string().allow('', null),
reporterUsername: Joi.string().allow('', null),
offenderHashedId: Joi.string().allow('', null),
offenderRandomId: Joi.string().allow('', null),
offenderUsername: Joi.string().allow('', null),
incidentAt: Joi.date().iso().required(),
chatHistory: Joi.array().min(1).required(),
metadata: Joi.object().unknown(true).optional()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const result = await chatService.reportChatIncident(value);
return res.status(201).json(result);
} catch (err) {
console.error('Error in reportChatIncident:', err);
return res.status(400).json({ error: err.message });
}
}
} }
export default ChatController; export default ChatController;

View File

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

View File

@@ -1,5 +1,4 @@
import FalukantService from '../services/falukantService.js'; import FalukantService from '../services/falukantService.js';
import politicalPowersService from '../services/falukantPoliticalPowersService.js';
function extractHashedUserId(req) { function extractHashedUserId(req) {
return req.headers?.userid; return req.headers?.userid;
@@ -27,51 +26,45 @@ class FalukantController {
}, { successStatus: 201 }); }, { successStatus: 201 });
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId)); this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId)); this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId), { blockInDebtorsPrison: true }); this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId));
this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId)); this.getBranchTypes = this._wrapWithUser((userId) => this.service.getBranchTypes(userId));
this.getBranch = this._wrapWithUser((userId, req) => this.service.getBranch(userId, req.params.branch)); 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), { blockInDebtorsPrison: true }); this.upgradeBranch = this._wrapWithUser((userId, req) => this.service.upgradeBranch(userId, req.body.branchId));
this.createProduction = this._wrapWithUser((userId, req) => { this.createProduction = this._wrapWithUser((userId, req) => {
const { branchId, productId, quantity } = req.body; const { branchId, productId, quantity } = req.body;
return this.service.createProduction(userId, branchId, productId, quantity); return this.service.createProduction(userId, branchId, productId, quantity);
}, { successStatus: 201, blockInDebtorsPrison: true }); }, { successStatus: 201 });
this.getProduction = this._wrapWithUser((userId, req) => this.service.getProduction(userId, req.params.branchId)); 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.getStock = this._wrapWithUser((userId, req) => this.service.getStock(userId, req.params.branchId || null));
this.createStock = this._wrapWithUser((userId, req) => { this.createStock = this._wrapWithUser((userId, req) => {
const { branchId, stockTypeId, stockSize } = req.body; const { branchId, stockTypeId, stockSize } = req.body;
return this.service.createStock(userId, branchId, stockTypeId, stockSize); return this.service.createStock(userId, branchId, stockTypeId, stockSize);
}, { successStatus: 201, blockInDebtorsPrison: true }); }, { successStatus: 201 });
this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId)); this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId));
this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId)); this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId));
this.sellProduct = this._wrapWithUser((userId, req) => { this.sellProduct = this._wrapWithUser((userId, req) => {
const { branchId, productId, quality, quantity } = req.body; const { branchId, productId, quality, quantity } = req.body;
return this.service.sellProduct(userId, branchId, productId, quality, quantity); return this.service.sellProduct(userId, branchId, productId, quality, quantity);
}, { successStatus: 201, blockInDebtorsPrison: true }); }, { successStatus: 201 });
this.sellAllProducts = this._wrapWithUser((userId, req) => { this.sellAllProducts = this._wrapWithUser((userId, req) => {
const { branchId } = req.body; const { branchId } = req.body;
return this.service.sellAllProducts(userId, branchId); return this.service.sellAllProducts(userId, branchId);
}, { successStatus: 201, blockInDebtorsPrison: true }); }, { successStatus: 201 });
this.moneyHistory = this._wrapWithUser((userId, req) => { this.moneyHistory = this._wrapWithUser((userId, req) => {
let { page, filter } = req.body; let { page, filter } = req.body;
if (!page) page = 1; if (!page) page = 1;
return this.service.moneyHistory(userId, page, filter); return this.service.moneyHistory(userId, page, filter);
}); });
this.moneyHistoryGraph = this._wrapWithUser((userId, req) => {
const { range } = req.body || {};
return this.service.moneyHistoryGraph(userId, range || '24h');
});
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId)); this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
this.buyStorage = this._wrapWithUser((userId, req) => { this.buyStorage = this._wrapWithUser((userId, req) => {
const { branchId, amount, stockTypeId } = req.body; const { branchId, amount, stockTypeId } = req.body;
return this.service.buyStorage(userId, branchId, amount, stockTypeId); return this.service.buyStorage(userId, branchId, amount, stockTypeId);
}, { successStatus: 201, blockInDebtorsPrison: true }); }, { successStatus: 201 });
this.sellStorage = this._wrapWithUser((userId, req) => { this.sellStorage = this._wrapWithUser((userId, req) => {
const { branchId, amount, stockTypeId } = req.body; const { branchId, amount, stockTypeId } = req.body;
return this.service.sellStorage(userId, branchId, amount, stockTypeId); return this.service.sellStorage(userId, branchId, amount, stockTypeId);
}, { successStatus: 202, blockInDebtorsPrison: true }); }, { successStatus: 202 });
this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes()); this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes());
this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview()); this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview());
@@ -81,157 +74,83 @@ class FalukantController {
console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId); console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId);
return this.service.getDirectorProposals(userId, req.body.branchId); return this.service.getDirectorProposals(userId, req.body.branchId);
}); });
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId), { blockInDebtorsPrison: true }); this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId));
this.getDirectorForBranch = this._wrapWithUser((userId, req) => this.service.getDirectorForBranch(userId, req.params.branchId)); this.getDirectorForBranch = this._wrapWithUser((userId, req) => this.service.getDirectorForBranch(userId, req.params.branchId));
this.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId)); this.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId));
this.updateDirector = this._wrapWithUser((userId, req) => { this.updateDirector = this._wrapWithUser((userId, req) => {
const { directorId, income } = req.body; const { directorId, income } = req.body;
return this.service.updateDirector(userId, directorId, income); return this.service.updateDirector(userId, directorId, income);
}, { blockInDebtorsPrison: true }); });
this.setSetting = this._wrapWithUser((userId, req) => { this.setSetting = this._wrapWithUser((userId, req) => {
const { branchId, directorId, settingKey, value } = req.body; const { branchId, directorId, settingKey, value } = req.body;
return this.service.setSetting(userId, branchId, directorId, settingKey, value); return this.service.setSetting(userId, branchId, directorId, settingKey, value);
}, { blockInDebtorsPrison: true }); });
this.getFamily = this._wrapWithUser(async (userId) => { this.getFamily = this._wrapWithUser(async (userId) => {
const result = await this.service.getFamily(userId); const result = await this.service.getFamily(userId);
if (!result) throw { status: 404, message: 'No family data found' }; if (!result) throw { status: 404, message: 'No family data found' };
return result; return result;
}); });
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId)); this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true }); this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true }); this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { 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) => { this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId); console.log('🔍 getGifts called with userId:', userId);
return this.service.getGifts(userId); return this.service.getGifts(userId);
}); });
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
this.improveLoverAffection = this._wrapWithUser((userId, req) =>
this.service.improveLoverAffection(userId, req.params.relationshipId), { 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.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
this.sendGift = this._wrapWithUser(async (userId, req) => { this.sendGift = this._wrapWithUser((userId, req) => this.service.sendGift(userId, req.body.giftId));
try {
return await this.service.sendGift(userId, req.body.giftId);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooOften') {
throw { status: 412, message: 'tooOften', retryAt: e.meta?.retryAt };
}
throw e;
}
}, { blockInDebtorsPrison: true });
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId)); this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) =>
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201, blockInDebtorsPrison: true });
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId)); this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId)); this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId)); this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId)); this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(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, blockInDebtorsPrison: true }); this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201 });
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.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
this.createParty = this._wrapWithUser((userId, req) => { this.createParty = this._wrapWithUser((userId, req) => {
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body; const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio); return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
}, { successStatus: 201, blockInDebtorsPrison: true }); }, { successStatus: 201 });
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId)); this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) => {
const { actionTypeId } = req.body;
return this.service.executeReputationAction(userId, actionTypeId);
}, { successStatus: 201 });
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId)); this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
this.baptise = this._wrapWithUser((userId, req) => { this.baptise = this._wrapWithUser((userId, req) => {
const { characterId: childId, firstName } = req.body; const { characterId: childId, firstName } = req.body;
return this.service.baptise(userId, childId, firstName); return this.service.baptise(userId, childId, firstName);
}, { 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.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
this.sendToSchool = this._wrapWithUser((userId, req) => { this.sendToSchool = this._wrapWithUser((userId, req) => {
const { item, student, studentId } = req.body; const { item, student, studentId } = req.body;
return this.service.sendToSchool(userId, item, student, studentId); return this.service.sendToSchool(userId, item, student, studentId);
}, { blockInDebtorsPrison: true }); });
this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId)); this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId));
this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId)); this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId));
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height), { blockInDebtorsPrison: true }); this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height));
this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId)); this.getNobility = this._wrapWithUser((userId) => this.service.getNobility(userId));
this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId), { blockInDebtorsPrison: true }); this.advanceNobility = this._wrapWithUser((userId) => this.service.advanceNobility(userId));
this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId)); this.getHealth = this._wrapWithUser((userId) => this.service.getHealth(userId));
this.healthActivity = this._wrapWithUser(async (userId, req) => { this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr));
try {
return await this.service.healthActivity(userId, req.body.measureTr);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
}
throw e;
}
}, { blockInDebtorsPrison: true });
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId)); this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getPoliticalOfficeCatalog = this._wrapWithUser((userId) => this.service.getPoliticalOfficeCatalog(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId)); this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId)); this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true }); 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), { blockInDebtorsPrison: true }); this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
this.getPoliticalMyPowers = this._wrapWithUser((userId) => politicalPowersService.getMyPowers(userId));
this.getPoliticalTaxJurisdiction = this._wrapWithUser((userId) => politicalPowersService.getTaxJurisdiction(userId));
this.setPoliticalRegionTax = this._wrapWithUser((userId, req) =>
politicalPowersService.setRegionTax(userId, parseInt(req.params.regionId, 10), req.body?.percent), { blockInDebtorsPrison: true });
this.getPoliticalRegionTaxHistory = this._wrapWithUser((userId, req) =>
politicalPowersService.getRegionTaxHistory(userId, parseInt(req.params.regionId, 10), parseInt(req.query.limit || '5', 10)));
this.getPoliticalAppointableOffices = this._wrapWithUser((userId) => politicalPowersService.getAppointableOffices(userId));
this.createPoliticalAppointment = this._wrapWithUser(
(userId, req) =>
politicalPowersService.createAppointment(userId, {
targetCharacterId: req.body?.targetCharacterId,
officeTypeId: req.body?.officeTypeId,
regionId: req.body?.regionId
}),
{ successStatus: 201, blockInDebtorsPrison: true }
);
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId)); this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId)); this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
@@ -243,15 +162,6 @@ class FalukantController {
} }
return this.service.getProductPriceInRegion(userId, productId, regionId); return this.service.getProductPriceInRegion(userId, productId, regionId);
}); });
this.getAllProductPricesInRegion = this._wrapWithUser((userId, req) => {
const regionId = parseInt(req.query.regionId, 10);
if (Number.isNaN(regionId)) {
throw new Error('regionId is required');
}
const networkWorth = req.query.networkWorth === '1' || req.query.networkWorth === 'true';
const branchId = req.query.branchId != null ? parseInt(req.query.branchId, 10) : null;
return this.service.getAllProductPricesInRegion(userId, regionId, { networkWorth, branchId });
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => { this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10); const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice); const currentPrice = parseFloat(req.query.currentPrice);
@@ -261,32 +171,13 @@ class FalukantController {
} }
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId); return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
}); });
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => { this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
const body = req.body || {}; this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
const items = Array.isArray(body.items) ? body.items : [];
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
const includeTransportCosts = body.includeTransportCosts === true || body.includeTransportCosts === 'true';
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,
{ includeTransportCosts }
);
});
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.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.getNotifications = this._wrapWithUser((userId) => this.service.getNotifications(userId));
this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size)); this.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 }); this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId)); this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
this.searchUsers = this._wrapWithUser((userId, req) => { this.searchUsers = this._wrapWithUser((userId, req) => {
@@ -307,7 +198,7 @@ class FalukantController {
throw { status: 400, message: 'goal is required for corrupt_politician' }; throw { status: 400, message: 'goal is required for corrupt_politician' };
} }
return this.service.createUndergroundActivity(userId, payload); return this.service.createUndergroundActivity(userId, payload);
}, { successStatus: 201, blockInDebtorsPrison: true }); }, { successStatus: 201 });
this.getUndergroundAttacks = this._wrapWithUser((userId, req) => { this.getUndergroundAttacks = this._wrapWithUser((userId, req) => {
const direction = (req.query.direction || '').toLowerCase(); const direction = (req.query.direction || '').toLowerCase();
@@ -321,14 +212,14 @@ class FalukantController {
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId)); this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
this.buyVehicles = this._wrapWithUser( this.buyVehicles = this._wrapWithUser(
(userId, req) => this.service.buyVehicles(userId, req.body), (userId, req) => this.service.buyVehicles(userId, req.body),
{ successStatus: 201, blockInDebtorsPrison: true } { successStatus: 201 }
); );
this.getVehicles = this._wrapWithUser( this.getVehicles = this._wrapWithUser(
(userId, req) => this.service.getVehicles(userId, req.query.regionId) (userId, req) => this.service.getVehicles(userId, req.query.regionId)
); );
this.createTransport = this._wrapWithUser( this.createTransport = this._wrapWithUser(
(userId, req) => this.service.createTransport(userId, req.body), (userId, req) => this.service.createTransport(userId, req.body),
{ successStatus: 201, blockInDebtorsPrison: true } { successStatus: 201 }
); );
this.getTransportRoute = this._wrapWithUser( this.getTransportRoute = this._wrapWithUser(
(userId, req) => this.service.getTransportRoute(userId, req.query) (userId, req) => this.service.getTransportRoute(userId, req.query)
@@ -338,40 +229,31 @@ class FalukantController {
); );
this.repairVehicle = this._wrapWithUser( this.repairVehicle = this._wrapWithUser(
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId), (userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
{ successStatus: 200, blockInDebtorsPrison: true } { successStatus: 200 }
); );
this.repairAllVehicles = this._wrapWithUser( this.repairAllVehicles = this._wrapWithUser(
(userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds), (userId, req) => this.service.repairAllVehicles(userId, req.body.vehicleIds),
{ successStatus: 200, blockInDebtorsPrison: true } { successStatus: 200 }
); );
} }
_wrapWithUser(fn, { successStatus = 200, postProcess, blockInDebtorsPrison = false } = {}) { _wrapWithUser(fn, { successStatus = 200, postProcess } = {}) {
return async (req, res) => { return async (req, res) => {
try { try {
const hashedUserId = extractHashedUserId(req); const hashedUserId = extractHashedUserId(req);
if (!hashedUserId) { if (!hashedUserId) {
return res.status(400).json({ error: 'Missing user identifier' }); return res.status(400).json({ error: 'Missing user identifier' });
} }
if (blockInDebtorsPrison) {
await this.service.assertActionAllowedOutsideDebtorsPrison(hashedUserId);
}
const result = await fn(hashedUserId, req, res); const result = await fn(hashedUserId, req, res);
const toSend = postProcess ? postProcess(result) : result; const toSend = postProcess ? postProcess(result) : result;
res.status(successStatus).json(toSend); res.status(successStatus).json(toSend);
} catch (error) { } catch (error) {
console.error('Controller error:', error); console.error('Controller error:', error);
const status = error.status && typeof error.status === 'number' ? error.status : 500; const status = error.status && typeof error.status === 'number' ? error.status : 500;
// Wenn error ein Objekt mit status ist, alle Felder außer status übernehmen
if (error && typeof error === 'object' && error.status && typeof error.status === 'number') {
const { status: errorStatus, ...errorData } = error;
res.status(errorStatus).json({ error: error.message || errorData.message || 'Internal error', ...errorData });
} else {
res.status(status).json({ error: error.message || 'Internal error' }); res.status(status).json({ error: error.message || 'Internal error' });
} }
}
}; };
} }

View File

@@ -1,79 +0,0 @@
import Joi from 'joi';
import moderationService from '../services/moderationService.js';
const moderationController = {
async createReport(req, res) {
const allowedTargetTypes = [
'forum_message',
'gallery_image',
'guestbook_entry',
'one_to_one_message',
'diary_entry',
'user_profile',
'blog',
'blog_post'
];
const schema = Joi.object({
targetType: Joi.string().valid(...allowedTargetTypes).required(),
targetId: Joi.number().integer().min(1).optional(),
targetRef: Joi.string().trim().max(255).allow('').optional(),
reason: Joi.string().trim().min(3).max(120).required(),
details: Joi.string().allow('').max(2000).optional()
}).or('targetId', 'targetRef');
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: userId } = req.headers;
const result = await moderationService.createReport(userId, value);
return res.status(201).json(result);
} catch (err) {
console.error('Error in createReport:', err);
return res.status(400).json({ error: err.message });
}
},
async listReports(req, res) {
try {
const { userid: userId } = req.headers;
const result = await moderationService.listReports(userId, req.query || {});
return res.status(200).json(result);
} catch (err) {
console.error('Error in listReports:', err);
return res.status(400).json({ error: err.message });
}
},
async updateReportStatus(req, res) {
const schema = Joi.object({
status: Joi.string().valid('open', 'in_review', 'resolved', 'rejected').required(),
reviewerNote: Joi.string().allow('').max(2000).optional()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
const { userid: userId } = req.headers;
const result = await moderationService.updateReportStatus(userId, req.params.reportId, value);
return res.status(200).json(result);
} catch (err) {
console.error('Error in updateReportStatus:', err);
return res.status(400).json({ error: err.message });
}
},
async getOpenReportCount(req, res) {
try {
const { userid: userId } = req.headers;
const result = await moderationService.getOpenReportCount(userId);
return res.status(200).json(result);
} catch (err) {
console.error('Error in getOpenReportCount:', err);
return res.status(400).json({ error: err.message });
}
}
};
export default moderationController;

View File

@@ -50,6 +50,11 @@ const menuStructure = {
visible: ["all"], visible: ["all"],
path: "/socialnetwork/gallery" path: "/socialnetwork/gallery"
}, },
vocabtrainer: {
visible: ["all"],
path: "/socialnetwork/vocab",
children: {}
},
blockedUsers: { blockedUsers: {
visible: ["all"], visible: ["all"],
path: "/socialnetwork/blocked" path: "/socialnetwork/blocked"
@@ -96,7 +101,9 @@ const menuStructure = {
}, },
eroticChat: { eroticChat: {
visible: ["over18"], visible: ["over18"],
action: "openEroticChat" action: "openEroticChat",
view: "window",
class: "eroticChatWindow"
} }
} }
}, },
@@ -176,30 +183,6 @@ const menuStructure = {
} }
} }
}, },
personal: {
visible: ["all"],
icon: "profile16.png",
children: {
sprachenlernen: {
visible: ["all"],
children: {
vocabtrainer: {
visible: ["all"],
path: "/socialnetwork/vocab",
showVocabLanguages: 1 // Flag für dynamische Sprachen-Liste
},
sprachkurse: {
visible: ["all"],
path: "/socialnetwork/vocab/courses"
}
}
},
calendar: {
visible: ["all"],
path: "/personal/calendar"
}
}
},
settings: { settings: {
visible: ["all"], visible: ["all"],
icon: "settings16.png", icon: "settings16.png",
@@ -212,10 +195,6 @@ const menuStructure = {
visible: ["all"], visible: ["all"],
path: "/settings/account" path: "/settings/account"
}, },
languageAssistant: {
visible: ["all"],
path: "/settings/language-assistant"
},
personal: { personal: {
visible: ["all"], visible: ["all"],
path: "/settings/personal" path: "/settings/personal"
@@ -256,14 +235,6 @@ const menuStructure = {
visible: ["mainadmin", "useradministration"], visible: ["mainadmin", "useradministration"],
path: "/admin/users" path: "/admin/users"
}, },
adultverification: {
visible: ["mainadmin", "useradministration"],
path: "/admin/users/adult-verification"
},
eroticmoderation: {
visible: ["mainadmin", "useradministration"],
path: "/admin/users/erotic-moderation"
},
userstatistics: { userstatistics: {
visible: ["mainadmin"], visible: ["mainadmin"],
path: "/admin/users/statistics" path: "/admin/users/statistics"
@@ -278,10 +249,6 @@ const menuStructure = {
visible: ["mainadmin", "forum"], visible: ["mainadmin", "forum"],
path: "/admin/forum" path: "/admin/forum"
}, },
moderationReports: {
visible: ["mainadmin", "forum"],
path: "/admin/moderation/reports"
},
chatrooms: { chatrooms: {
visible: ["mainadmin", "chatrooms"], visible: ["mainadmin", "chatrooms"],
path: "/admin/chatrooms" path: "/admin/chatrooms"
@@ -295,7 +262,7 @@ const menuStructure = {
path: "/admin/interests" path: "/admin/interests"
}, },
falukant: { falukant: {
visible: ["mainadmin", "falukant", "worker_schedule_read"], visible: ["mainadmin", "falukant"],
children: { children: {
logentries: { logentries: {
visible: ["mainadmin", "falukant"], visible: ["mainadmin", "falukant"],
@@ -317,10 +284,6 @@ const menuStructure = {
visible: ["mainadmin", "falukant"], visible: ["mainadmin", "falukant"],
path: "/admin/falukant/create-npc" path: "/admin/falukant/create-npc"
}, },
workerSchedules: {
visible: ["mainadmin", "worker_schedule_read"],
path: "/admin/falukant/worker-schedules"
},
} }
}, },
minigames: { minigames: {
@@ -357,14 +320,7 @@ class NavigationController {
return age; return age;
} }
normalizeAdultVerificationStatus(value) { async filterMenu(menu, rights, age, userId) {
if (['pending', 'approved', 'rejected'].includes(value)) {
return value;
}
return 'none';
}
async filterMenu(menu, rights, age, userId, adultVerificationStatus = 'none') {
const filteredMenu = {}; const filteredMenu = {};
try { try {
const hasFalukantAccount = await this.hasFalukantAccount(userId); const hasFalukantAccount = await this.hasFalukantAccount(userId);
@@ -378,17 +334,8 @@ class NavigationController {
|| (value.visible.includes('hasfalukantaccount') && hasFalukantAccount)) { || (value.visible.includes('hasfalukantaccount') && hasFalukantAccount)) {
const { visible, ...itemWithoutVisible } = value; const { visible, ...itemWithoutVisible } = value;
filteredMenu[key] = { ...itemWithoutVisible }; filteredMenu[key] = { ...itemWithoutVisible };
if (
value.visible.includes("over18")
&& age >= 18
&& adultVerificationStatus !== 'approved'
&& (value.path || value.action || value.view)
) {
filteredMenu[key].disabled = true;
filteredMenu[key].disabledReasonKey = 'socialnetwork.erotic.lockedShort';
}
if (value.children) { if (value.children) {
filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId, adultVerificationStatus); filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId);
} }
} }
} }
@@ -415,33 +362,37 @@ class NavigationController {
required: false required: false
}] }]
}); });
const userParams = await UserParam.findAll({ const userBirthdateParams = await UserParam.findAll({
where: { userId: user.id }, where: { userId: user.id },
include: [ include: [
{ {
model: UserParamType, model: UserParamType,
as: 'paramType', as: 'paramType',
where: { description: ['birthdate', 'adult_verification_status'] } where: { description: 'birthdate' }
} }
] ]
}); });
let birthDate = (new Date()).toDateString(); const birthDate = userBirthdateParams.length > 0 ? userBirthdateParams[0].value : (new Date()).toDateString();
let adultVerificationStatus = 'none';
for (const param of userParams) {
if (param.paramType?.description === 'birthdate' && param.value) {
birthDate = param.value;
}
if (param.paramType?.description === 'adult_verification_status') {
adultVerificationStatus = this.normalizeAdultVerificationStatus(param.value);
}
}
const age = this.calculateAge(birthDate); const age = this.calculateAge(birthDate);
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean); const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id, adultVerificationStatus); const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
// Vokabeltrainer: Sprachen werden im Frontend dynamisch geladen (wie Forum) // Dynamisches Submenü: Treffpunkt → Vokabeltrainer → (Neue Sprache + abonnierte/angelegte)
// Keine children mehr, da das Menü nur 2 Ebenen unterstützt // Wichtig: "Neue Sprache" soll IMMER sichtbar sein auch wenn die DB-Abfrage (noch) fehlschlägt.
// Das Frontend lädt die Sprachen separat und zeigt sie als submenu2 an if (filteredMenu?.socialnetwork?.children?.vocabtrainer) {
const children = {
newLanguage: { path: '/socialnetwork/vocab/new' },
};
try {
const langs = await this.vocabService.listLanguagesForMenu(user.id);
for (const l of langs) {
children[`lang_${l.id}`] = { path: `/socialnetwork/vocab/${l.id}`, label: l.name };
}
} catch (e) {
console.warn('[menu] Konnte Vokabeltrainer-Sprachen nicht laden:', e?.message || e);
}
filteredMenu.socialnetwork.children.vocabtrainer.children = children;
}
res.status(200).json(filteredMenu); res.status(200).json(filteredMenu);
} catch (error) { } catch (error) {

View File

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

View File

@@ -185,57 +185,6 @@ class SettingsController {
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
} }
async getLlmSettings(req, res) {
try {
const hashedUserId = req.headers.userid;
const data = await settingsService.getLlmSettings(hashedUserId);
res.status(200).json(data);
} catch (error) {
console.error('Error retrieving LLM settings:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
async saveLlmSettings(req, res) {
const schema = Joi.object({
baseUrl: Joi.string().allow('').optional(),
model: Joi.string().allow('').optional(),
enabled: Joi.boolean().optional(),
apiKey: Joi.string().allow('').optional(),
clearKey: Joi.boolean().optional()
});
const { error, value } = schema.validate(req.body || {});
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
try {
await settingsService.saveLlmSettings(req.headers.userid, value);
res.status(200).json({ success: true });
} catch (err) {
console.error('Error saving LLM settings:', err);
res.status(500).json({ error: 'Internal server error' });
}
}
async submitAdultVerificationRequest(req, res) {
try {
const hashedUserId = req.headers.userid;
const note = req.body?.note || '';
const file = req.file || null;
const result = await settingsService.submitAdultVerificationRequest(hashedUserId, { note }, file);
res.status(200).json(result);
} catch (error) {
console.error('Error submitting adult verification request:', error);
const status = [
'User not found',
'Adult verification can only be requested by adult users',
'No verification document provided',
'Unsupported verification document type'
].includes(error.message) ? 400 : 500;
res.status(status).json({ error: error.message });
}
}
} }
export default SettingsController; export default SettingsController;

View File

@@ -15,19 +15,6 @@ class SocialNetworkController {
this.changeImage = this.changeImage.bind(this); this.changeImage = this.changeImage.bind(this);
this.getFoldersByUsername = this.getFoldersByUsername.bind(this); this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
this.deleteFolder = this.deleteFolder.bind(this); this.deleteFolder = this.deleteFolder.bind(this);
this.getAdultFolders = this.getAdultFolders.bind(this);
this.getAdultFoldersByUsername = this.getAdultFoldersByUsername.bind(this);
this.createAdultFolder = this.createAdultFolder.bind(this);
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
this.uploadAdultImage = this.uploadAdultImage.bind(this);
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
this.changeAdultImage = this.changeAdultImage.bind(this);
this.listEroticVideos = this.listEroticVideos.bind(this);
this.getEroticVideosByUsername = this.getEroticVideosByUsername.bind(this);
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
this.changeEroticVideo = this.changeEroticVideo.bind(this);
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
this.reportEroticContent = this.reportEroticContent.bind(this);
this.createGuestbookEntry = this.createGuestbookEntry.bind(this); this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
this.getGuestbookEntries = this.getGuestbookEntries.bind(this); this.getGuestbookEntries = this.getGuestbookEntries.bind(this);
this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this); this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this);
@@ -160,8 +147,8 @@ class SocialNetworkController {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;
const { imageId } = req.params; const { imageId } = req.params;
const { title, visibilities, selectedUsers } = req.body; const { title, visibilities } = req.body;
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities, selectedUsers); const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities);
console.log('--->', folderId); console.log('--->', folderId);
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId)); res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
} catch (error) { } catch (error) {
@@ -200,177 +187,6 @@ class SocialNetworkController {
} }
} }
async getAdultFolders(req, res) {
try {
const userId = req.headers.userid;
const folders = await this.socialNetworkService.getAdultFolders(userId);
res.status(200).json(folders);
} catch (error) {
console.error('Error in getAdultFolders:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async getAdultFoldersByUsername(req, res) {
try {
const requestingUserId = req.headers.userid;
const { username } = req.params;
const folders = await this.socialNetworkService.getAdultFoldersByUsername(username, requestingUserId);
if (!folders) {
return res.status(404).json({ error: 'No folders found or access denied.' });
}
res.status(200).json(folders);
} catch (error) {
console.error('Error in getAdultFoldersByUsername:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async createAdultFolder(req, res) {
try {
const userId = req.headers.userid;
const folderData = req.body;
const { folderId } = req.params;
const folder = await this.socialNetworkService.createAdultFolder(userId, folderData, folderId);
res.status(201).json(folder);
} catch (error) {
console.error('Error in createAdultFolder:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async getAdultFolderImageList(req, res) {
try {
const userId = req.headers.userid;
const { folderId } = req.params;
const images = await this.socialNetworkService.getAdultFolderImageList(userId, folderId);
res.status(200).json(images);
} catch (error) {
console.error('Error in getAdultFolderImageList:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async uploadAdultImage(req, res) {
try {
const userId = req.headers.userid;
const file = req.file;
const formData = req.body;
const image = await this.socialNetworkService.uploadAdultImage(userId, file, formData);
res.status(201).json(image);
} catch (error) {
console.error('Error in uploadAdultImage:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async getAdultImageByHash(req, res) {
try {
const userId = req.headers.userid;
const { hash } = req.params;
const filePath = await this.socialNetworkService.getAdultImageFilePath(userId, hash);
res.sendFile(filePath, err => {
if (err) {
console.error('Error sending adult file:', err);
res.status(500).json({ error: 'Error sending file' });
}
});
} catch (error) {
console.error('Error in getAdultImageByHash:', error);
res.status(error.status || 403).json({ error: error.message || 'Access denied or image not found' });
}
}
async changeAdultImage(req, res) {
try {
const userId = req.headers.userid;
const { imageId } = req.params;
const { title, visibilities, selectedUsers } = req.body;
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities, selectedUsers);
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
} catch (error) {
console.error('Error in changeAdultImage:', error);
res.status(error.status || 403).json({ error: error.message || 'Access denied or image not found' });
}
}
async listEroticVideos(req, res) {
try {
const userId = req.headers.userid;
const videos = await this.socialNetworkService.listEroticVideos(userId);
res.status(200).json(videos);
} catch (error) {
console.error('Error in listEroticVideos:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async getEroticVideosByUsername(req, res) {
try {
const userId = req.headers.userid;
const { username } = req.params;
const videos = await this.socialNetworkService.getEroticVideosByUsername(username, userId);
res.status(200).json(videos);
} catch (error) {
console.error('Error in getEroticVideosByUsername:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async uploadEroticVideo(req, res) {
try {
const userId = req.headers.userid;
const file = req.file;
const formData = req.body;
const video = await this.socialNetworkService.uploadEroticVideo(userId, file, formData);
res.status(201).json(video);
} catch (error) {
console.error('Error in uploadEroticVideo:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async changeEroticVideo(req, res) {
try {
const userId = req.headers.userid;
const { videoId } = req.params;
const updatedVideo = await this.socialNetworkService.changeEroticVideo(userId, videoId, req.body);
res.status(200).json(updatedVideo);
} catch (error) {
console.error('Error in changeEroticVideo:', error);
res.status(error.status || 500).json({ error: error.message });
}
}
async getEroticVideoByHash(req, res) {
try {
const userId = req.headers.userid;
const { hash } = req.params;
const { filePath, mimeType } = await this.socialNetworkService.getEroticVideoFilePath(userId, hash);
res.type(mimeType);
res.sendFile(filePath, err => {
if (err) {
console.error('Error sending adult video:', err);
res.status(500).json({ error: 'Error sending file' });
}
});
} catch (error) {
console.error('Error in getEroticVideoByHash:', error);
res.status(error.status || 403).json({ error: error.message || 'Access denied or video not found' });
}
}
async reportEroticContent(req, res) {
try {
const userId = req.headers.userid;
const result = await this.socialNetworkService.createEroticContentReport(userId, req.body || {});
res.status(201).json(result);
} catch (error) {
console.error('Error in reportEroticContent:', error);
res.status(error.status || 400).json({ error: error.message });
}
}
async createGuestbookEntry(req, res) { async createGuestbookEntry(req, res) {
try { try {
const { htmlContent, recipientName } = req.body; const { htmlContent, recipientName } = req.body;

View File

@@ -9,7 +9,6 @@ class VocabController {
this.service = new VocabService(); this.service = new VocabService();
this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId)); this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId));
this.listAllLanguages = this._wrapWithUser(() => this.service.listAllLanguages());
this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 }); this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 });
this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 }); this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 });
this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId)); this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId));
@@ -18,66 +17,10 @@ class VocabController {
this.createChapter = this._wrapWithUser((userId, req) => this.service.createChapter(userId, req.params.languageId, req.body), { successStatus: 201 }); 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.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.searchVocabs = this._wrapWithUser((userId, req) => this.service.searchVocabs(userId, req.params.languageId, req.query));
this.getLanguageDictionary = this._wrapWithUser((userId, req) =>
this.service.getLanguageDictionary(userId, req.params.languageId, req.query)
);
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId)); this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId)); this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 }); this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
this.getLessonVocabPool = this._wrapWithUser((userId, req) => this.service.getLessonVocabPool(userId, req.params.lessonId));
// 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.getCompletedLessonVocabPool = this._wrapWithUser((userId, req) =>
this.service.getCompletedLessonVocabPool(userId, req.params.courseId, req.query.untilLessonId)
);
this.getCourseDictionary = this._wrapWithUser((userId, req) =>
this.service.getCourseDictionary(userId, req.params.courseId, req.query)
);
this.getVocabDistractorPool = this._wrapWithUser((userId, req) =>
this.service.getVocabDistractorPool(userId, req.params.courseId, req.query.beforeLessonId)
);
this.getCourseSrsDue = this._wrapWithUser((userId, req) =>
this.service.getCourseSrsDue(userId, req.params.courseId, req.query)
);
this.reviewSrsItem = this._wrapWithUser((userId, req) =>
this.service.reviewSrsItem(userId, req.body),
{ successStatus: 201 }
);
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));
this.getDashboardLearningSummary = this._wrapWithUser((userId) => this.service.getDashboardLearningSummary(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));
this.resetLessonProgress = this._wrapWithUser((userId, req) => this.service.resetMyLessonProgress(userId, req.params.lessonId));
// 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));
this.sendLessonAssistantMessage = this._wrapWithUser((userId, req) => this.service.sendLessonAssistantMessage(userId, req.params.lessonId, req.body), { successStatus: 201 });
} }
_wrapWithUser(fn, { successStatus = 200 } = {}) { _wrapWithUser(fn, { successStatus = 200 } = {}) {
@@ -99,3 +42,5 @@ class VocabController {
} }
export default VocabController; export default VocabController;

View File

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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
# Kopie nach `backend/.env` — nicht committen.
#
# Produktion / direkter DB-Host steht typischerweise in `.env`.
# Für Entwicklung mit SSH-Tunnel: Datei `backend/.env.local` anlegen (wird nach `.env`
# geladen und überschreibt). So bleibt `.env` mit echtem Host, Tunnel nur lokal.
#
# Beispiel backend/.env.local:
# DB_HOST=127.0.0.1
# DB_PORT=60000
# # DB_SSL=0
# (Tunnel z. B.: ssh -L 60000:127.0.0.1:5432 user@server)
#
DB_NAME=
DB_USER=
DB_PASS=
# DB_HOST=
# DB_PORT=5432
# DB_SSL=0
#
# Optional (Defaults siehe utils/sequelize.js)
# DB_CONNECT_TIMEOUT_MS=30000

View File

@@ -1,5 +0,0 @@
# Kopie nach backend/.env.local (liegt neben .env, wird nicht committet).
# Überschreibt nur bei dir lokal z. B. SSH-Tunnel — .env kann weiter DB_HOST=tsschulz.de haben.
DB_HOST=127.0.0.1
DB_PORT=60000

View File

@@ -1,94 +0,0 @@
/**
* Periodischer Job: reputation_periodic für politische Amtsinhaber.
* Aufruf: systemd-Timer oder FALUKANT_POLITICAL_REPUTATION_JOB=1 (siehe server.js).
*/
import PoliticalOffice from '../models/falukant/data/political_office.js';
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
import PoliticalOfficeBenefit from '../models/falukant/predefine/political_office_benefit.js';
import PoliticalOfficeBenefitType from '../models/falukant/type/political_office_benefit_type.js';
import PoliticalBenefitLastTick from '../models/falukant/data/political_benefit_last_tick.js';
import FalukantCharacter from '../models/falukant/data/character.js';
import FalukantUser from '../models/falukant/data/user.js';
import User from '../models/community/user.js';
import { sequelize } from '../utils/sequelize.js';
import { notifyUser } from '../utils/socket.js';
export async function runPoliticalReputationTicks() {
const offices = await PoliticalOffice.findAll({
include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['id', 'name'] }]
});
const toNotify = new Set();
let ticks = 0;
for (const po of offices) {
const characterId = po.characterId;
const benefitRows = await PoliticalOfficeBenefit.findAll({
where: { officeTypeId: po.officeTypeId },
include: [
{
model: PoliticalOfficeBenefitType,
as: 'benefitDefinition',
attributes: ['tr'],
required: true,
where: { tr: 'reputation_periodic' }
}
]
});
for (const br of benefitRows) {
const v = br.value && typeof br.value === 'object' ? br.value : {};
const intervalDays = Math.max(1, Number(v.intervalDays ?? v.everyDays ?? 7));
const gain = Math.max(1, Number(v.gain ?? 1));
const [tickRow, created] = await PoliticalBenefitLastTick.findOrCreate({
where: {
characterId,
politicalOfficeBenefitId: br.id
},
defaults: {
characterId,
politicalOfficeBenefitId: br.id,
lastTickAt: new Date(po.createdAt),
ticksCount: 0
}
});
const baseMs = new Date(tickRow.lastTickAt).getTime();
const daysSince = Math.floor((Date.now() - baseMs) / 86400000);
if (daysSince < intervalDays) continue;
await sequelize.transaction(async (t) => {
const ch = await FalukantCharacter.findByPk(characterId, { transaction: t });
if (!ch) return;
const nextRep = Math.min(100, (ch.reputation ?? 0) + gain);
await ch.update({ reputation: nextRep }, { transaction: t });
await PoliticalBenefitLastTick.update(
{
lastTickAt: new Date(),
ticksCount: (tickRow.ticksCount || 0) + 1
},
{ where: { id: tickRow.id }, transaction: t }
);
});
ticks += 1;
toNotify.add(characterId);
console.info(
`[PoliticalBenefits] reputation_tick characterId=${characterId} benefitId=${br.id} gain=${gain}`
);
}
}
for (const characterId of toNotify) {
const ch = await FalukantCharacter.findByPk(characterId, { attributes: ['userId'] });
if (!ch?.userId) continue;
const fu = await FalukantUser.findOne({
where: { id: ch.userId },
include: [{ model: User, as: 'user', attributes: ['hashedId'] }]
});
const hid = fu?.user?.hashedId;
if (hid) notifyUser(hid, 'falukantUpdateStatus', {});
}
return { processedOffices: offices.length, ticksApplied: ticks };
}

View File

@@ -10,9 +10,6 @@ export const authenticate = async (req, res, next) => {
if (!user) { if (!user) {
return res.status(401).json({ error: 'Unauthorized: Invalid credentials' }); return res.status(401).json({ error: 'Unauthorized: Invalid credentials' });
} }
if (!user.active) {
return res.status(403).json({ error: 'Unauthorized: User blocked' });
}
try { try {
await updateUserTimestamp(user.id); await updateUserTimestamp(user.id);
} catch (error) { } catch (error) {

View File

@@ -1,45 +0,0 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
const table = { tableName: 'contact_message', schema: 'service' };
const columns = await queryInterface.describeTable(table);
if (!columns.answer) {
await queryInterface.addColumn(table, 'answer', {
type: Sequelize.TEXT,
allowNull: true
});
}
if (!columns.answered_at) {
await queryInterface.addColumn(table, 'answered_at', {
type: Sequelize.DATE,
allowNull: true
});
}
if (!columns.is_answered) {
await queryInterface.addColumn(table, 'is_answered', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
}
},
down: async (queryInterface, Sequelize) => {
const table = { tableName: 'contact_message', schema: 'service' };
const columns = await queryInterface.describeTable(table);
if (columns.answer) {
await queryInterface.removeColumn(table, 'answer');
}
if (columns.answered_at) {
await queryInterface.removeColumn(table, 'answered_at');
}
if (columns.is_answered) {
await queryInterface.removeColumn(table, 'is_answered');
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,122 +0,0 @@
/* 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;
`);
},
};

View File

@@ -1,60 +0,0 @@
/* 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');
`);
},
};

View File

@@ -1,53 +0,0 @@
'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');
}
};

View File

@@ -1,36 +0,0 @@
'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'
);
}
};

View File

@@ -1,83 +0,0 @@
'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');
}
};

View File

@@ -1,22 +0,0 @@
'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;
`);
}
};

View File

@@ -1,36 +0,0 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS learning_goals JSONB,
ADD COLUMN IF NOT EXISTS core_patterns JSONB,
ADD COLUMN IF NOT EXISTS grammar_focus JSONB,
ADD COLUMN IF NOT EXISTS speaking_prompts JSONB,
ADD COLUMN IF NOT EXISTS practical_tasks JSONB;
`);
await queryInterface.sequelize.query(`
INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES
('dialog_completion', 'Dialogergänzung'),
('situational_response', 'Situative Antwort'),
('pattern_drill', 'Muster-Drill'),
('reading_aloud', 'Lautlese-Übung'),
('speaking_from_memory', 'Freies Sprechen')
ON CONFLICT (name) DO NOTHING;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS practical_tasks,
DROP COLUMN IF EXISTS speaking_prompts,
DROP COLUMN IF EXISTS grammar_focus,
DROP COLUMN IF EXISTS core_patterns,
DROP COLUMN IF EXISTS learning_goals;
`);
}
};

View File

@@ -1,18 +0,0 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.user_param
ALTER COLUMN value TYPE TEXT;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.user_param
ALTER COLUMN value TYPE VARCHAR(255);
`);
}
};

View File

@@ -1,31 +0,0 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'community', tableName: 'folder' },
'is_adult_area',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
);
await queryInterface.addColumn(
{ schema: 'community', tableName: 'image' },
'is_adult_content',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
);
},
async down(queryInterface) {
await queryInterface.removeColumn({ schema: 'community', tableName: 'image' }, 'is_adult_content');
await queryInterface.removeColumn({ schema: 'community', tableName: 'folder' }, 'is_adult_area');
},
};

View File

@@ -1,63 +0,0 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
title: {
type: Sequelize.STRING,
allowNull: false,
},
description: {
type: Sequelize.TEXT,
allowNull: true,
},
original_file_name: {
type: Sequelize.STRING,
allowNull: false,
},
hash: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
mime_type: {
type: Sequelize.STRING,
allowNull: false,
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'user' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
}
);
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video' });
},
};

View File

@@ -1,20 +0,0 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'chat', tableName: 'room' },
'is_adult_only',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
);
},
async down(queryInterface) {
await queryInterface.removeColumn({ schema: 'chat', tableName: 'room' }, 'is_adult_only');
},
};

View File

@@ -1,95 +0,0 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn(
{ schema: 'community', tableName: 'image' },
'is_moderated_hidden',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
}
).catch(() => {});
await queryInterface.addColumn(
{ schema: 'community', tableName: 'erotic_video' },
'is_moderated_hidden',
{
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
}
).catch(() => {});
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_content_report' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
reporter_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: { model: { schema: 'community', tableName: 'user' }, key: 'id' },
onDelete: 'CASCADE'
},
target_type: {
type: Sequelize.STRING(20),
allowNull: false
},
target_id: {
type: Sequelize.INTEGER,
allowNull: false
},
reason: {
type: Sequelize.STRING(80),
allowNull: false
},
note: {
type: Sequelize.TEXT,
allowNull: true
},
status: {
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'open'
},
action_taken: {
type: Sequelize.STRING(40),
allowNull: true
},
handled_by: {
type: Sequelize.INTEGER,
allowNull: true,
references: { model: { schema: 'community', tableName: 'user' }, key: 'id' },
onDelete: 'SET NULL'
},
handled_at: {
type: Sequelize.DATE,
allowNull: true
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('NOW()')
},
updated_at: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal('NOW()')
}
}
).catch(() => {});
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_content_report' }).catch(() => {});
await queryInterface.removeColumn({ schema: 'community', tableName: 'erotic_video' }, 'is_moderated_hidden').catch(() => {});
await queryInterface.removeColumn({ schema: 'community', tableName: 'image' }, 'is_moderated_hidden').catch(() => {});
}
};

View File

@@ -1,89 +0,0 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_image_visibility' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
visibility_type_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'type', tableName: 'image_visibility' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.createTable(
{ schema: 'community', tableName: 'erotic_video_visibility_user' },
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
erotic_video_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'erotic_video' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: { schema: 'community', tableName: 'user' },
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
}
);
await queryInterface.sequelize.query(`
INSERT INTO community.erotic_video_image_visibility (erotic_video_id, visibility_type_id)
SELECT ev.id, iv.id
FROM community.erotic_video ev
CROSS JOIN type.image_visibility iv
WHERE iv.description = 'adults'
AND NOT EXISTS (
SELECT 1
FROM community.erotic_video_image_visibility eviv
WHERE eviv.erotic_video_id = ev.id
AND eviv.visibility_type_id = iv.id
)
`);
},
async down(queryInterface) {
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_visibility_user' });
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_image_visibility' });
},
};

View File

@@ -1,36 +0,0 @@
"use strict";
/** Schwangerschaft (Admin / Spiel): erwarteter Geburtstermin + optionaler Vater-Charakter */
module.exports = {
async up(queryInterface) {
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 = 'pregnancy_due_at'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_due_at TIMESTAMPTZ NULL;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_father_character_id'
) THEN
ALTER TABLE falukant_data."character"
ADD COLUMN pregnancy_father_character_id INTEGER NULL
REFERENCES falukant_data."character"(id) ON DELETE SET NULL;
END IF;
END$$;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_father_character_id;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_due_at;
`);
},
};

View File

@@ -1,44 +0,0 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
ADD COLUMN IF NOT EXISTS didactic_mode TEXT,
ADD COLUMN IF NOT EXISTS phase_label TEXT,
ADD COLUMN IF NOT EXISTS block_number INTEGER,
ADD COLUMN IF NOT EXISTS difficulty_weight INTEGER,
ADD COLUMN IF NOT EXISTS new_unit_target INTEGER,
ADD COLUMN IF NOT EXISTS review_weight INTEGER,
ADD COLUMN IF NOT EXISTS is_intensive_review BOOLEAN NOT NULL DEFAULT FALSE;
COMMENT ON COLUMN community.vocab_course_lesson.didactic_mode IS
'Didaktischer Modus der Lektion, z.B. core_input, guided_dialogue, intensive_review oder checkpoint.';
COMMENT ON COLUMN community.vocab_course_lesson.phase_label IS
'Übergeordnete Lernphase, z.B. quickstart, daily_life oder stabilization.';
COMMENT ON COLUMN community.vocab_course_lesson.block_number IS
'Inhaltlicher Block für Konsolidierungs- und Wiederholungswellen.';
COMMENT ON COLUMN community.vocab_course_lesson.difficulty_weight IS
'Grobe relative Schwierigkeit der Lektion von leicht bis schwer.';
COMMENT ON COLUMN community.vocab_course_lesson.new_unit_target IS
'Empfohlene Zahl neuer Spracheinheiten in dieser Lektion.';
COMMENT ON COLUMN community.vocab_course_lesson.review_weight IS
'Wie stark Wiederholung in dieser Lektion dominieren soll, typischerweise 0 bis 100.';
COMMENT ON COLUMN community.vocab_course_lesson.is_intensive_review IS
'Markiert Lektionen, die als intensive Wiederholungsphase gedacht sind.';
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_lesson
DROP COLUMN IF EXISTS is_intensive_review,
DROP COLUMN IF EXISTS review_weight,
DROP COLUMN IF EXISTS new_unit_target,
DROP COLUMN IF EXISTS difficulty_weight,
DROP COLUMN IF EXISTS block_number,
DROP COLUMN IF EXISTS phase_label,
DROP COLUMN IF EXISTS didactic_mode;
`);
}
};

View File

@@ -1,20 +0,0 @@
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_progress
ADD COLUMN IF NOT EXISTS lesson_state JSONB NOT NULL DEFAULT '{}'::jsonb;
COMMENT ON COLUMN community.vocab_course_progress.lesson_state IS
'Persistierter UI- und Lernzustand pro Nutzer und Lektion fuer Resume im Sprachkurs.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE community.vocab_course_progress
DROP COLUMN IF EXISTS lesson_state;
`);
}
};

View File

@@ -1,72 +0,0 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on date NULL;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) THEN
ALTER TABLE falukant_predefine.political_office_benefit
RENAME COLUMN political_office_id TO office_type_id;
ELSIF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) THEN
UPDATE falukant_predefine.political_office_benefit
SET office_type_id = COALESCE(office_type_id, political_office_id);
ALTER TABLE falukant_predefine.political_office_benefit
DROP COLUMN political_office_id;
END IF;
END $$;
`);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
DROP COLUMN IF EXISTS last_political_daily_salary_on;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'office_type_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_predefine'
AND table_name = 'political_office_benefit'
AND column_name = 'political_office_id'
) THEN
ALTER TABLE falukant_predefine.political_office_benefit
RENAME COLUMN office_type_id TO political_office_id;
END IF;
END $$;
`);
}
};

View File

@@ -1,30 +0,0 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
INSERT INTO type.user_param_value (user_param_type_id, value, order_id)
SELECT upt.id, 'fr', COALESCE(
(SELECT MAX(v.order_id) FROM type.user_param_value v WHERE v.user_param_type_id = upt.id),
0
) + 1
FROM type.user_param upt
WHERE upt.description = 'language'
AND NOT EXISTS (
SELECT 1 FROM type.user_param_value x
WHERE x.user_param_type_id = upt.id AND x.value = 'fr'
);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DELETE FROM type.user_param_value v
USING type.user_param upt
WHERE v.user_param_type_id = upt.id
AND upt.description = 'language'
AND v.value = 'fr';
`);
},
};

View File

@@ -1,22 +0,0 @@
/* eslint-disable */
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
ADD COLUMN IF NOT EXISTS certificate_productions_count_since TIMESTAMPTZ;
`);
await queryInterface.sequelize.query(`
COMMENT ON COLUMN falukant_data.falukant_user.certificate_productions_count_since IS
'Daemon/UI: Zählt nur falukant_log.production-Zeilen mit COALESCE(production_timestamp, production_date::timestamp) >= diesem Wert; bei Stufenänderung (Aufstieg/Bankrott/Erbfolge) auf NOW() (YpDaemon QUERY_UPDATE_FALUKANT_USER_CERTIFICATE). NULL = alle passenden Log-Zeilen bis zur ersten Stufenänderung nach Migration. Kein Löschen der Logs zum Reset.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.falukant_user
DROP COLUMN IF EXISTS certificate_productions_count_since;
`);
}
};

View File

@@ -1,61 +0,0 @@
'use strict';
/** @param {import('sequelize').QueryInterface} queryInterface */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.political_benefit_last_tick (
id serial PRIMARY KEY,
character_id integer NOT NULL
REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
political_office_benefit_id integer NOT NULL
REFERENCES falukant_predefine.political_office_benefit(id) ON DELETE CASCADE,
last_tick_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
ticks_count integer NOT NULL DEFAULT 0,
CONSTRAINT political_benefit_last_tick_unique UNIQUE (character_id, political_office_benefit_id)
);
CREATE INDEX IF NOT EXISTS political_benefit_last_tick_character_idx
ON falukant_data.political_benefit_last_tick (character_id);
`);
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.region_tax_history (
id serial PRIMARY KEY,
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
old_tax_percent numeric(12,4) NOT NULL,
new_tax_percent numeric(12,4) NOT NULL,
setter_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS region_tax_history_region_idx
ON falukant_data.region_tax_history (region_id, created_at DESC);
`);
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_data.political_appointment (
id serial PRIMARY KEY,
appointer_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
target_character_id integer NOT NULL REFERENCES falukant_data."character"(id) ON DELETE CASCADE,
office_type_id integer NOT NULL REFERENCES falukant_type.political_office_type(id) ON DELETE CASCADE,
region_id integer NOT NULL REFERENCES falukant_data.region(id) ON DELETE CASCADE,
status varchar(32) NOT NULL DEFAULT 'completed',
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at timestamptz NULL,
completed_political_office_id integer NULL REFERENCES falukant_data.political_office(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS political_appointment_appointer_idx
ON falukant_data.political_appointment (appointer_character_id, created_at DESC);
CREATE INDEX IF NOT EXISTS political_appointment_target_idx
ON falukant_data.political_appointment (target_character_id);
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS falukant_data.political_appointment;
DROP TABLE IF EXISTS falukant_data.region_tax_history;
DROP TABLE IF EXISTS falukant_data.political_benefit_last_tick;
`);
}
};

View File

@@ -1,50 +0,0 @@
'use strict';
/** Stufe pro politischem Amt (Tageshonorar: base + perRank × hierarchy_level). */
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.political_office_type
ADD COLUMN IF NOT EXISTS hierarchy_level INTEGER NOT NULL DEFAULT 1;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_type.political_office_type AS pot
SET hierarchy_level = sub.lvl
FROM (VALUES
('assessor', 1),
('councillor', 1),
('council', 2),
('beadle', 2),
('town-clerk', 2),
('mayor', 3),
('master-builder', 2),
('village-major', 2),
('judge', 3),
('bailif', 3),
('taxman', 2),
('sheriff', 3),
('consultant', 3),
('treasurer', 4),
('hangman', 2),
('territorial-council', 3),
('territorial-council-speaker', 4),
('ruler-consultant', 4),
('state-administrator', 4),
('super-state-administrator', 5),
('governor', 5),
('ministry-helper', 4),
('minister', 5),
('chancellor', 6)
) AS sub(name, lvl)
WHERE pot.name = sub.name;
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_type.political_office_type
DROP COLUMN IF EXISTS hierarchy_level;
`);
}
};

View File

@@ -1,32 +0,0 @@
/* eslint-disable */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD COLUMN IF NOT EXISTS scandal_extra_daily_pct double precision NOT NULL DEFAULT 0;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD CONSTRAINT relationship_state_scandal_extra_daily_pct_chk
CHECK (scandal_extra_daily_pct >= 0 AND scandal_extra_daily_pct <= 100);
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_scandal_extra_daily_pct_chk;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP COLUMN IF EXISTS scandal_extra_daily_pct;
`);
},
};

View File

@@ -1,54 +0,0 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ADD COLUMN IF NOT EXISTS marriage_satisfaction integer;
`);
await queryInterface.sequelize.query(`
UPDATE falukant_data.relationship_state
SET marriage_satisfaction = 55
WHERE marriage_satisfaction IS NULL;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ALTER COLUMN marriage_satisfaction SET DEFAULT 55;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
ALTER COLUMN marriage_satisfaction SET NOT NULL;
`);
await queryInterface.sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'relationship_state_marriage_satisfaction_check'
AND connamespace = 'falukant_data'::regnamespace
) THEN
ALTER TABLE falukant_data.relationship_state
ADD CONSTRAINT relationship_state_marriage_satisfaction_check
CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100);
END IF;
END $$;
`);
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP CONSTRAINT IF EXISTS relationship_state_marriage_satisfaction_check;
`);
await queryInterface.sequelize.query(`
ALTER TABLE falukant_data.relationship_state
DROP COLUMN IF EXISTS marriage_satisfaction;
`);
}
};

View File

@@ -1,49 +0,0 @@
'use strict';
module.exports = {
async up(queryInterface) {
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS community.vocab_srs_item (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES community."user"(id) ON DELETE CASCADE,
course_id INTEGER NOT NULL REFERENCES community.vocab_course(id) ON DELETE CASCADE,
lesson_id INTEGER NULL REFERENCES community.vocab_course_lesson(id) ON DELETE SET NULL,
item_key VARCHAR(80) NOT NULL,
learning TEXT NOT NULL,
reference TEXT NOT NULL,
direction VARCHAR(8) NOT NULL DEFAULT 'BOTH',
stage INTEGER NOT NULL DEFAULT 0,
interval_days INTEGER NOT NULL DEFAULT 0,
last_reviewed_at TIMESTAMP WITH TIME ZONE NULL,
next_due_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
correct_count INTEGER NOT NULL DEFAULT 0,
wrong_count INTEGER NOT NULL DEFAULT 0,
lapse_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT vocab_srs_item_user_key_unique UNIQUE (user_id, item_key)
);
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_due
ON community.vocab_srs_item (user_id, course_id, next_due_at);
CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_lesson
ON community.vocab_srs_item (user_id, course_id, lesson_id);
COMMENT ON TABLE community.vocab_srs_item IS
'Nutzerbezogener SRS-Fortschritt pro Vokabel/Phrase aus Sprachkursen.';
COMMENT ON COLUMN community.vocab_srs_item.item_key IS
'Stabiler deterministischer Schlüssel aus Kurs, Lektion und normalisiertem Begriffspaar.';
COMMENT ON COLUMN community.vocab_srs_item.stage IS
'SRS-Stufe. Höhere Stufen bedeuten längere Wiederholungsintervalle.';
COMMENT ON COLUMN community.vocab_srs_item.next_due_at IS
'Zeitpunkt, zu dem das Item wieder fällig ist.';
`);
},
async down(queryInterface) {
await queryInterface.sequelize.query(`
DROP TABLE IF EXISTS community.vocab_srs_item;
`);
}
};

View File

@@ -0,0 +1,43 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answer', {
type: Sequelize.TEXT,
allowNull: true
});
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answered_at', {
type: Sequelize.DATE,
allowNull: true
});
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'is_answered', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answer');
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answered_at');
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'is_answered');
}
};

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
# Backend-Migrationen (Sequelize)
## Aktive Migrationen
Neue Schema-Änderungen: nur noch Dateien unter **`migrations-active/`**. Ausführung z.B.:
`npm run db:migrate` (siehe `backend/package.json`, Pfad `migrations-active`).
## Archiv
Ältere, bereits auf den Umgebungen ausgerollte Migrationen liegen unter **`migrations-archive/`** und werden vom Sequelize-CLI **nicht** mehr ausgeführt.
Vor dem ersten Deploy nach dieser Aufteilung: fehlende Einträge in `"SequelizeMeta"` für die archivierten Dateinamen per SQL ergänzen, siehe **`sql/baseline-sequelize-meta-migrations-archive.sql`** (idempotent).
## Sonstiges in diesem Ordner
SQL-Hilfsdateien und ältere Notizen verbleiben hier (werden nicht vom CLI geladen).
## Falukant: Zertifikat und Produktionszählung
| Datei (Archiv) | Inhalt |
|--------|--------|
| `migrations-archive/20260402140000-add-certificate-productions-count-since.cjs` | Spalte `falukant_data.falukant_user.certificate_productions_count_since` (`TIMESTAMPTZ`, nullable) inkl. Kommentar. Setzt die DB-Grundlage dafür, dass Daemon, Backend und UI dieselbe Periode für „abgeschlossene Produktionen“ nutzen (Filter mit `COALESCE(production_timestamp, production_date::timestamp)` ab diesem Zeitpunkt; `NULL` = bisherige Historie). |
Eine parallele SQL-Migration im Daemon-Repository (z.B. `014_falukant_certificate_productions_count_since.sql`) kann dieselbe Spalte anlegen, wenn das Deployment dort getrennt ist Schema doppelt anlegen vermeiden.
Details zur Zähl- und Retention-Logik: `docs/FALUKANT_PRODUCTION_CERTIFICATE.md`.

View File

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

View File

@@ -5,7 +5,6 @@ import ChatUser from './chat/user.js';
import Room from './chat/room.js'; import Room from './chat/room.js';
import User from './community/user.js'; import User from './community/user.js';
import UserParam from './community/user_param.js'; import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js';
import UserParamType from './type/user_param.js'; import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js'; import UserRightType from './type/user_right.js';
import UserRight from './community/user_right.js'; import UserRight from './community/user_right.js';
@@ -18,15 +17,11 @@ import UserParamVisibilityType from './type/user_param_visibility.js';
import UserParamVisibility from './community/user_param_visibility.js'; import UserParamVisibility from './community/user_param_visibility.js';
import Folder from './community/folder.js'; import Folder from './community/folder.js';
import Image from './community/image.js'; import Image from './community/image.js';
import EroticVideo from './community/erotic_video.js';
import EroticContentReport from './community/erotic_content_report.js';
import ImageVisibilityType from './type/image_visibility.js'; import ImageVisibilityType from './type/image_visibility.js';
import ImageVisibilityUser from './community/image_visibility_user.js'; import ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js'; import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.js'; import ImageImageVisibility from './community/image_image_visibility.js';
import FolderVisibilityUser from './community/folder_visibility_user.js'; import FolderVisibilityUser from './community/folder_visibility_user.js';
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
import GuestbookEntry from './community/guestbook.js'; import GuestbookEntry from './community/guestbook.js';
import Forum from './forum/forum.js'; import Forum from './forum/forum.js';
import Title from './forum/title.js'; import Title from './forum/title.js';
@@ -49,7 +44,6 @@ import FalukantStockType from './falukant/type/stock.js';
import Knowledge from './falukant/data/product_knowledge.js'; import Knowledge from './falukant/data/product_knowledge.js';
import ProductType from './falukant/type/product.js'; import ProductType from './falukant/type/product.js';
import TitleOfNobility from './falukant/type/title_of_nobility.js'; import TitleOfNobility from './falukant/type/title_of_nobility.js';
import TitleBenefit from './falukant/type/title_benefit.js';
import TitleRequirement from './falukant/type/title_requirement.js'; import TitleRequirement from './falukant/type/title_requirement.js';
import Branch from './falukant/data/branch.js'; import Branch from './falukant/data/branch.js';
import BranchType from './falukant/type/branch.js'; import BranchType from './falukant/type/branch.js';
@@ -72,7 +66,6 @@ import PromotionalGiftCharacterTrait from './falukant/predefine/promotional_gift
import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js'; import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js';
import RelationshipType from './falukant/type/relationship.js'; import RelationshipType from './falukant/type/relationship.js';
import Relationship from './falukant/data/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 PromotionalGiftLog from './falukant/log/promotional_gift.js';
import HouseType from './falukant/type/house.js'; import HouseType from './falukant/type/house.js';
import BuyableHouse from './falukant/data/buyable_house.js'; import BuyableHouse from './falukant/data/buyable_house.js';
@@ -94,19 +87,12 @@ import Candidate from './falukant/data/candidate.js';
import Vote from './falukant/data/vote.js'; import Vote from './falukant/data/vote.js';
import PoliticalOfficeType from './falukant/type/political_office_type.js'; import PoliticalOfficeType from './falukant/type/political_office_type.js';
import PoliticalOffice from './falukant/data/political_office.js'; import PoliticalOffice from './falukant/data/political_office.js';
import PoliticalBenefitLastTick from './falukant/data/political_benefit_last_tick.js';
import RegionTaxHistory from './falukant/data/region_tax_history.js';
import PoliticalAppointment from './falukant/data/political_appointment.js';
import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js'; import PoliticalOfficeBenefit from './falukant/predefine/political_office_benefit.js';
import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js'; import PoliticalOfficeBenefitType from './falukant/type/political_office_benefit_type.js';
import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js'; import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js';
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js'; import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
import PoliticalOfficeHistory from './falukant/log/political_office_history.js'; import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js'; import ElectionHistory from './falukant/log/election_history.js';
import ChurchOfficeType from './falukant/type/church_office_type.js';
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
import ChurchOffice from './falukant/data/church_office.js';
import ChurchApplication from './falukant/data/church_application.js';
import Underground from './falukant/data/underground.js'; import Underground from './falukant/data/underground.js';
import UndergroundType from './falukant/type/underground.js'; import UndergroundType from './falukant/type/underground.js';
import VehicleType from './falukant/type/vehicle.js'; import VehicleType from './falukant/type/vehicle.js';
@@ -116,18 +102,8 @@ import RegionDistance from './falukant/data/region_distance.js';
import WeatherType from './falukant/type/weather.js'; import WeatherType from './falukant/type/weather.js';
import Weather from './falukant/data/weather.js'; import Weather from './falukant/data/weather.js';
import ProductWeatherEffect from './falukant/type/product_weather_effect.js'; import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
import ProductPriceHistory from './falukant/log/product_price_history.js';
import Blog from './community/blog.js'; import Blog from './community/blog.js';
import BlogPost from './community/blog_post.js'; import BlogPost from './community/blog_post.js';
import VocabCourse from './community/vocab_course.js';
import VocabCourseLesson from './community/vocab_course_lesson.js';
import VocabCourseEnrollment from './community/vocab_course_enrollment.js';
import VocabCourseProgress from './community/vocab_course_progress.js';
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
import VocabSrsItem from './community/vocab_srs_item.js';
import CalendarEvent from './community/calendar_event.js';
import Campaign from './match3/campaign.js'; import Campaign from './match3/campaign.js';
import Match3Level from './match3/level.js'; import Match3Level from './match3/level.js';
import Objective from './match3/objective.js'; import Objective from './match3/objective.js';
@@ -179,9 +155,6 @@ export default function setupAssociations() {
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' }); User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' }); UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasOne(UserDashboard, { foreignKey: 'userId', as: 'dashboard' });
UserDashboard.belongsTo(User, { foreignKey: 'userId', as: 'user' });
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' }); UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' }); UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
@@ -217,14 +190,6 @@ export default function setupAssociations() {
Image.belongsTo(User, { foreignKey: 'userId' }); Image.belongsTo(User, { foreignKey: 'userId' });
User.hasMany(Image, { foreignKey: 'userId' }); User.hasMany(Image, { foreignKey: 'userId' });
EroticVideo.belongsTo(User, { foreignKey: 'userId', as: 'owner' });
User.hasMany(EroticVideo, { foreignKey: 'userId', as: 'eroticVideos' });
EroticContentReport.belongsTo(User, { foreignKey: 'reporterId', as: 'reporter' });
User.hasMany(EroticContentReport, { foreignKey: 'reporterId', as: 'eroticContentReports' });
EroticContentReport.belongsTo(User, { foreignKey: 'handledBy', as: 'moderator' });
User.hasMany(EroticContentReport, { foreignKey: 'handledBy', as: 'handledEroticContentReports' });
// Image visibility associations // Image visibility associations
Folder.belongsToMany(ImageVisibilityType, { Folder.belongsToMany(ImageVisibilityType, {
through: FolderImageVisibility, through: FolderImageVisibility,
@@ -248,17 +213,6 @@ export default function setupAssociations() {
otherKey: 'imageId' otherKey: 'imageId'
}); });
EroticVideo.belongsToMany(ImageVisibilityType, {
through: EroticVideoImageVisibility,
foreignKey: 'eroticVideoId',
otherKey: 'visibilityTypeId'
});
ImageVisibilityType.belongsToMany(EroticVideo, {
through: EroticVideoImageVisibility,
foreignKey: 'visibilityTypeId',
otherKey: 'eroticVideoId'
});
Folder.belongsToMany(ImageVisibilityUser, { Folder.belongsToMany(ImageVisibilityUser, {
through: FolderVisibilityUser, through: FolderVisibilityUser,
foreignKey: 'folderId', foreignKey: 'folderId',
@@ -270,19 +224,6 @@ export default function setupAssociations() {
otherKey: 'folderId' otherKey: 'folderId'
}); });
EroticVideo.belongsToMany(User, {
through: EroticVideoVisibilityUser,
foreignKey: 'eroticVideoId',
otherKey: 'userId',
as: 'selectedVisibilityUsers'
});
User.belongsToMany(EroticVideo, {
through: EroticVideoVisibilityUser,
foreignKey: 'userId',
otherKey: 'eroticVideoId',
as: 'visibleEroticVideos'
});
// Guestbook related associations // Guestbook related associations
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' }); User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' }); User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
@@ -383,8 +324,6 @@ export default function setupAssociations() {
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' }); FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' }); RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
FalukantCharacter.belongsTo(FalukantCharacter, { foreignKey: 'pregnancyFatherCharacterId', as: 'pregnancyFather' });
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' }); FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' }); FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
@@ -396,8 +335,6 @@ export default function setupAssociations() {
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' }); TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' }); TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
TitleOfNobility.hasMany(TitleBenefit, { foreignKey: 'titleId', as: 'benefits' });
TitleBenefit.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' }); Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' }); RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });
@@ -468,13 +405,6 @@ export default function setupAssociations() {
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' }); DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' }); FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' });
// Produkt-Preishistorie (Zeitreihe für Preiskurven)
ProductPriceHistory.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
ProductType.hasMany(ProductPriceHistory, { foreignKey: 'productId', as: 'priceHistory' });
ProductPriceHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(ProductPriceHistory, { foreignKey: 'regionId', as: 'productPriceHistory' });
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' }); Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' }); FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
@@ -503,8 +433,6 @@ export default function setupAssociations() {
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', }); Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', });
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', }); FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', }); 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' }); PromotionalGiftLog.belongsTo(PromotionalGift, { foreignKey: 'giftId', as: 'gift' });
PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' }); PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' });
@@ -630,14 +558,14 @@ export default function setupAssociations() {
Party.belongsToMany(TitleOfNobility, { Party.belongsToMany(TitleOfNobility, {
through: PartyInvitedNobility, through: PartyInvitedNobility,
foreignKey: 'partyId', foreignKey: 'party_id',
otherKey: 'titleOfNobilityId', otherKey: 'title_of_nobility_id',
as: 'invitedNobilities', as: 'invitedNobilities',
}); });
TitleOfNobility.belongsToMany(Party, { TitleOfNobility.belongsToMany(Party, {
through: PartyInvitedNobility, through: PartyInvitedNobility,
foreignKey: 'titleOfNobilityId', foreignKey: 'title_of_nobility_id',
otherKey: 'partyId', otherKey: 'party_id',
as: 'partiesInvitedTo', as: 'partiesInvitedTo',
}); });
@@ -796,48 +724,6 @@ export default function setupAssociations() {
as: 'heldOffice' as: 'heldOffice'
}); });
PoliticalBenefitLastTick.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'character'
});
FalukantCharacter.hasMany(PoliticalBenefitLastTick, {
foreignKey: 'characterId',
as: 'politicalBenefitTicks'
});
PoliticalBenefitLastTick.belongsTo(PoliticalOfficeBenefit, {
foreignKey: 'politicalOfficeBenefitId',
as: 'officeBenefit'
});
RegionTaxHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(RegionTaxHistory, { foreignKey: 'regionId', as: 'taxHistory' });
RegionTaxHistory.belongsTo(FalukantCharacter, {
foreignKey: 'setterCharacterId',
as: 'setterCharacter'
});
RegionTaxHistory.belongsTo(PoliticalOffice, {
foreignKey: 'politicalOfficeId',
as: 'sourceOffice'
});
PoliticalAppointment.belongsTo(FalukantCharacter, {
foreignKey: 'appointerCharacterId',
as: 'appointer'
});
PoliticalAppointment.belongsTo(FalukantCharacter, {
foreignKey: 'targetCharacterId',
as: 'targetCharacter'
});
PoliticalAppointment.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
PoliticalAppointment.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
PoliticalAppointment.belongsTo(PoliticalOffice, {
foreignKey: 'completedPoliticalOfficeId',
as: 'completedOffice'
});
// elections // elections
Election.belongsTo(PoliticalOfficeType, { Election.belongsTo(PoliticalOfficeType, {
foreignKey: 'officeTypeId', foreignKey: 'officeTypeId',
@@ -973,96 +859,6 @@ export default function setupAssociations() {
} }
); );
// — Church Offices —
// Requirements for church office
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
ChurchOfficeType.hasMany(ChurchOfficeRequirement, {
foreignKey: 'officeTypeId',
as: 'requirements'
});
// Prerequisite office type
ChurchOfficeRequirement.belongsTo(ChurchOfficeType, {
foreignKey: 'prerequisiteOfficeTypeId',
as: 'prerequisiteOfficeType'
});
// Actual church office holdings
ChurchOffice.belongsTo(ChurchOfficeType, {
foreignKey: 'officeTypeId',
as: 'type'
});
ChurchOfficeType.hasMany(ChurchOffice, {
foreignKey: 'officeTypeId',
as: 'offices'
});
ChurchOffice.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'holder'
});
FalukantCharacter.hasOne(ChurchOffice, {
foreignKey: 'characterId',
as: 'heldChurchOffice'
});
// Supervisor relationship
ChurchOffice.belongsTo(FalukantCharacter, {
foreignKey: 'supervisorId',
as: 'supervisor'
});
// Region relationship
ChurchOffice.belongsTo(RegionData, {
foreignKey: 'regionId',
as: 'region'
});
RegionData.hasMany(ChurchOffice, {
foreignKey: 'regionId',
as: 'churchOffices'
});
// Applications for church office
ChurchApplication.belongsTo(ChurchOfficeType, {
foreignKey: 'officeTypeId',
as: 'officeType'
});
ChurchOfficeType.hasMany(ChurchApplication, {
foreignKey: 'officeTypeId',
as: 'applications'
});
ChurchApplication.belongsTo(FalukantCharacter, {
foreignKey: 'characterId',
as: 'applicant'
});
FalukantCharacter.hasMany(ChurchApplication, {
foreignKey: 'characterId',
as: 'churchApplications'
});
ChurchApplication.belongsTo(FalukantCharacter, {
foreignKey: 'supervisorId',
as: 'supervisor'
});
FalukantCharacter.hasMany(ChurchApplication, {
foreignKey: 'supervisorId',
as: 'supervisedApplications'
});
ChurchApplication.belongsTo(RegionData, {
foreignKey: 'regionId',
as: 'region'
});
RegionData.hasMany(ChurchApplication, {
foreignKey: 'regionId',
as: 'churchApplications'
});
Underground.belongsTo(UndergroundType, { Underground.belongsTo(UndergroundType, {
foreignKey: 'undergroundTypeId', foreignKey: 'undergroundTypeId',
as: 'undergroundType' as: 'undergroundType'
@@ -1145,47 +941,5 @@ export default function setupAssociations() {
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' }); TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' }); TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' });
// Vocab Course associations
VocabCourse.belongsTo(User, { foreignKey: 'ownerUserId', as: 'owner' });
User.hasMany(VocabCourse, { foreignKey: 'ownerUserId', as: 'ownedCourses' });
VocabCourseLesson.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseLesson, { foreignKey: 'courseId', as: 'lessons' });
VocabCourseEnrollment.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabCourseEnrollment, { foreignKey: 'userId', as: 'courseEnrollments' });
VocabCourseEnrollment.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseEnrollment, { foreignKey: 'courseId', as: 'enrollments' });
VocabCourseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabCourseProgress, { foreignKey: 'userId', as: 'courseProgress' });
VocabCourseProgress.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseProgress, { foreignKey: 'courseId', as: 'progress' });
VocabCourseProgress.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabCourseProgress, { foreignKey: 'lessonId', as: 'progress' });
// Grammar Exercise associations
VocabGrammarExercise.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabGrammarExercise, { foreignKey: 'lessonId', as: 'grammarExercises' });
VocabGrammarExercise.belongsTo(VocabGrammarExerciseType, { foreignKey: 'exerciseTypeId', as: 'exerciseType' });
VocabGrammarExerciseType.hasMany(VocabGrammarExercise, { foreignKey: 'exerciseTypeId', as: 'exercises' });
VocabGrammarExercise.belongsTo(User, { foreignKey: 'createdByUserId', as: 'creator' });
User.hasMany(VocabGrammarExercise, { foreignKey: 'createdByUserId', as: 'createdGrammarExercises' });
VocabGrammarExerciseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
VocabSrsItem.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabSrsItem, { foreignKey: 'userId', as: 'vocabSrsItems' });
VocabSrsItem.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabSrsItem, { foreignKey: 'courseId', as: 'srsItems' });
VocabSrsItem.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabSrsItem, { foreignKey: 'lessonId', as: 'srsItems' });
// Calendar associations
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(CalendarEvent, { foreignKey: 'userId', as: 'calendarEvents' });
} }

View File

@@ -20,10 +20,6 @@ const Room = sequelize.define('Room', {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: true}, defaultValue: true},
isAdultOnly: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false},
genderRestrictionId: { genderRestrictionId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true}, allowNull: true},

View File

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

View File

@@ -1,64 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class EroticContentReport extends Model {}
EroticContentReport.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
reporterId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'reporter_id'
},
targetType: {
type: DataTypes.STRING,
allowNull: false,
field: 'target_type'
},
targetId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'target_id'
},
reason: {
type: DataTypes.STRING,
allowNull: false
},
note: {
type: DataTypes.TEXT,
allowNull: true
},
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'open'
},
actionTaken: {
type: DataTypes.STRING,
allowNull: true,
field: 'action_taken'
},
handledBy: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'handled_by'
},
handledAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'handled_at'
}
}, {
sequelize,
modelName: 'EroticContentReport',
tableName: 'erotic_content_report',
schema: 'community',
timestamps: true,
underscored: true
});
export default EroticContentReport;

View File

@@ -1,55 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class EroticVideo extends Model {}
EroticVideo.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
originalFileName: {
type: DataTypes.STRING,
allowNull: false,
field: 'original_file_name'
},
hash: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
mimeType: {
type: DataTypes.STRING,
allowNull: false,
field: 'mime_type'
},
isModeratedHidden: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'is_moderated_hidden'
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'user_id'
}
}, {
sequelize,
modelName: 'EroticVideo',
tableName: 'erotic_video',
schema: 'community',
timestamps: true,
underscored: true
});
export default EroticVideo;

View File

@@ -1,26 +0,0 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const EroticVideoImageVisibility = sequelize.define('erotic_video_image_visibility', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
eroticVideoId: {
type: DataTypes.INTEGER,
allowNull: false
},
visibilityTypeId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'erotic_video_image_visibility',
timestamps: false,
underscored: true,
schema: 'community'
});
export default EroticVideoImageVisibility;

View File

@@ -1,26 +0,0 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const EroticVideoVisibilityUser = sequelize.define('erotic_video_visibility_user', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
eroticVideoId: {
type: DataTypes.INTEGER,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'erotic_video_visibility_user',
timestamps: false,
underscored: true,
schema: 'community'
});
export default EroticVideoVisibilityUser;

View File

@@ -6,11 +6,6 @@ const Folder = sequelize.define('folder', {
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false}, allowNull: false},
isAdultArea: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
parentId: { parentId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: true

View File

@@ -6,16 +6,6 @@ const Image = sequelize.define('image', {
title: { title: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false}, allowNull: false},
isAdultContent: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
isModeratedHidden: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
description: { description: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true}, allowNull: true},

View File

@@ -3,66 +3,6 @@ import { DataTypes } from 'sequelize';
import { encrypt, decrypt } from '../../utils/encryption.js'; import { encrypt, decrypt } from '../../utils/encryption.js';
import crypto from 'crypto'; import crypto from 'crypto';
function encodeEncryptedValueToBlob(value) {
const encrypted = encrypt(value);
return Buffer.from(encrypted, 'utf8');
}
/** Nur echte Adressen zurückgeben — verhindert Anzeige von Base64-/Key-artigem Müll bei fehlender Entschlüsselung. */
function looksLikePlausibleEmail(s) {
if (typeof s !== 'string') {
return false;
}
const t = s.trim();
if (!t || t.length > 254) {
return false;
}
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test(t);
}
function normalizeEmailCandidate(s) {
if (!s || typeof s !== 'string') {
return null;
}
const t = s.trim();
return looksLikePlausibleEmail(t) ? t : null;
}
function decodeEncryptedBlob(value) {
if (!value) {
return null;
}
try {
const encryptedUtf8 = value.toString('utf8');
const decryptedUtf8 = decrypt(encryptedUtf8);
const fromUtf8 = normalizeEmailCandidate(decryptedUtf8);
if (fromUtf8) {
return fromUtf8;
}
} catch (error) {
console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message);
}
try {
const encryptedHex = value.toString('hex');
const decryptedHex = decrypt(encryptedHex);
const fromHex = normalizeEmailCandidate(decryptedHex);
if (fromHex) {
return fromHex;
}
} catch (error) {
console.warn('Email legacy hex decryption failed:', error.message);
}
try {
return normalizeEmailCandidate(value.toString('utf8'));
} catch (error) {
console.warn('Email could not be read as plain text:', error.message);
return null;
}
}
const User = sequelize.define('user', { const User = sequelize.define('user', {
email: { email: {
type: DataTypes.BLOB, type: DataTypes.BLOB,
@@ -70,12 +10,35 @@ const User = sequelize.define('user', {
unique: true, unique: true,
set(value) { set(value) {
if (value) { if (value) {
this.setDataValue('email', encodeEncryptedValueToBlob(value)); const encrypted = encrypt(value);
// Konvertiere Hex-String zu Buffer für die Speicherung
const buffer = Buffer.from(encrypted, 'hex');
this.setDataValue('email', buffer);
} }
}, },
get() { get() {
const encrypted = this.getDataValue('email'); const encrypted = this.getDataValue('email');
return decodeEncryptedBlob(encrypted); if (encrypted) {
try {
// Konvertiere Buffer zu String für die Entschlüsselung
const encryptedString = encrypted.toString('hex');
const decrypted = decrypt(encryptedString);
if (decrypted) {
return decrypted;
}
} catch (error) {
console.warn('Email decryption failed, treating as plain text:', error.message);
}
// Fallback: Versuche es als Klartext zu lesen
try {
return encrypted.toString('utf8');
} catch (error) {
console.warn('Email could not be read as plain text:', error.message);
return null;
}
}
return null;
} }
}, },
salt: { salt: {

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