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
483 changed files with 9269 additions and 80930 deletions

View File

@@ -1,34 +0,0 @@
name: Deploy to production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- 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
run: |
ssh -i ~/.ssh/id_ed25519 \
-p "${{ secrets.PROD_PORT }}" \
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
"/home/tsschulz/deploy-yourpart-bluegreen.sh"

8
.gitignore vendored
View File

@@ -5,9 +5,7 @@
.depbe.sh
node_modules
node_modules/*
# package-lock.json wird versioniert (npm ci im Deploy braucht konsistente Locks zu package.json)
backend/.env
backend/.env.local
backend/images
backend/images/*
backend/node_modules
@@ -19,9 +17,3 @@ frontend/dist
frontend/dist/*
frontedtree.txt
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 +0,0 @@
## zum testen des push

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

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

View File

@@ -7,90 +7,40 @@ import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
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
const productionEnvPath = '/opt/yourpart/backend/.env';
const localEnvPath = path.resolve(__dirname, '../.env');
let envPath = localEnvPath; // Fallback
let usingProduction = false;
if (fs.existsSync(productionEnvPath)) {
// Prüfe Lesbarkeit bevor wir versuchen, sie zu laden
try {
fs.accessSync(productionEnvPath, fs.constants.R_OK);
envPath = productionEnvPath;
usingProduction = true;
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;
}
console.log('[env] Lade Produktions-.env:', productionEnvPath);
} else {
log('[env] Produktions-.env nicht gefunden, lade lokale .env:', localEnvPath);
console.log('[env] Lade lokale .env:', localEnvPath);
}
// Lade .env-Datei (robust gegen Fehler)
log('[env] Versuche .env zu laden von:', envPath);
log('[env] Datei existiert:', fs.existsSync(envPath));
let result;
try {
result = dotenv.config({ path: envPath, quiet: dotenvQuiet });
if (result.error) {
warn('[env] Konnte .env nicht laden:', result.error.message);
warn('[env] Fehler-Details:', result.error);
} else {
log('[env] .env erfolgreich geladen von:', envPath, usingProduction ? '(production)' : '(local)');
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);
}
}
// Lade .env-Datei
console.log('[env] Versuche .env zu laden von:', envPath);
console.log('[env] Datei existiert:', fs.existsSync(envPath));
console.log('[env] Datei lesbar:', fs.accessSync ? (() => { try { fs.accessSync(envPath, fs.constants.R_OK); return true; } catch { return false; } })() : 'unbekannt');
const result = dotenv.config({ path: envPath });
if (result.error) {
console.warn('[env] Konnte .env nicht laden:', result.error.message);
console.warn('[env] Fehler-Details:', result.error);
} else {
console.log('[env] .env erfolgreich geladen von:', envPath);
console.log('[env] Geladene Variablen:', Object.keys(result.parsed || {}));
}
// Lokale Überschreibungen (nicht committen): z. B. SSH-Tunnel DB_HOST=127.0.0.1 DB_PORT=60000
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_HOST:', process.env.REDIS_HOST);
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_URL:', process.env.REDIS_URL);
}
// Debug: Zeige Redis-Konfiguration
console.log('[env] Redis-Konfiguration:');
console.log('[env] REDIS_HOST:', process.env.REDIS_HOST);
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_URL:', process.env.REDIS_URL);
if (!process.env.SECRET_KEY) {
warn('[env] SECRET_KEY nicht gesetzt in .env');
console.warn('[env] SECRET_KEY nicht gesetzt in .env');
}
export {};

View File

@@ -13,9 +13,6 @@ class AdminController {
this.searchUser = this.searchUser.bind(this);
this.getFalukantUserById = this.getFalukantUserById.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.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
this.updateFalukantStock = this.updateFalukantStock.bind(this);
this.addFalukantStock = this.addFalukantStock.bind(this);
@@ -32,12 +29,6 @@ class AdminController {
this.getUser = this.getUser.bind(this);
this.getUsers = this.getUsers.bind(this);
this.updateUser = this.updateUser.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
this.listRightTypes = this.listRightTypes.bind(this);
@@ -128,97 +119,6 @@ class AdminController {
}
}
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 ---
async listRightTypes(req, res) {
try {
@@ -375,50 +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 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 getFalukantUserBranches(req, res) {
try {
const { userid: userId } = req.headers;
@@ -667,7 +523,6 @@ class AdminController {
title: Joi.string().min(1).max(255).required(),
roomTypeId: Joi.number().integer().required(),
isPublic: Joi.boolean().required(),
isAdultOnly: Joi.boolean().allow(null),
genderRestrictionId: Joi.number().integer().allow(null),
minAge: Joi.number().integer().min(0).allow(null),
maxAge: Joi.number().integer().min(0).allow(null),
@@ -679,7 +534,7 @@ class AdminController {
if (error) {
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);
} catch (error) {
console.log(error);
@@ -698,7 +553,6 @@ class AdminController {
title: Joi.string().min(1).max(255).required(),
roomTypeId: Joi.number().integer().required(),
isPublic: Joi.boolean().required(),
isAdultOnly: Joi.boolean().allow(null),
genderRestrictionId: Joi.number().integer().allow(null),
minAge: Joi.number().integer().min(0).allow(null),
maxAge: Joi.number().integer().min(0).allow(null),
@@ -725,7 +579,7 @@ class AdminController {
if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) {
return res.status(403).json({ error: 'Keine Berechtigung.' });
}
await AdminService.deleteRoom(userId, req.params.id);
await AdminService.deleteRoom(req.params.id);
res.sendStatus(204);
} catch (error) {
console.log(error);

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,9 +13,6 @@ class ChatController {
this.sendOneToOneMessage = this.sendOneToOneMessage.bind(this);
this.getOneToOneMessageHistory = this.getOneToOneMessageHistory.bind(this);
this.getRoomList = this.getRoomList.bind(this);
this.getRoomCreateOptions = this.getRoomCreateOptions.bind(this);
this.getOwnRooms = this.getOwnRooms.bind(this);
this.deleteOwnRoom = this.deleteOwnRoom.bind(this);
}
async getMessages(req, res) {
@@ -172,49 +169,12 @@ class ChatController {
async getRoomList(req, res) {
// Öffentliche Räume für Chat-Frontend
try {
const { userid: hashedUserId } = req.headers;
const adultOnly = String(req.query.adultOnly || '').toLowerCase() === 'true';
const rooms = await chatService.getRoomList(hashedUserId, { adultOnly });
const rooms = await chatService.getRoomList();
res.status(200).json(rooms);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getRoomCreateOptions(req, res) {
try {
const options = await chatService.getRoomCreateOptions();
res.status(200).json(options);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
async getOwnRooms(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const rooms = await chatService.getOwnRooms(hashedUserId);
res.status(200).json(rooms);
} catch (error) {
const status = error.message === 'user_not_found' ? 404 : 500;
res.status(status).json({ error: error.message });
}
}
async deleteOwnRoom(req, res) {
try {
const { userid: hashedUserId } = req.headers;
const roomId = Number.parseInt(req.params.id, 10);
if (!Number.isInteger(roomId) || roomId <= 0) {
return res.status(400).json({ error: 'invalid_room_id' });
}
await chatService.deleteOwnRoom(hashedUserId, roomId);
res.status(204).send();
} catch (error) {
const status = error.message === 'room_not_found_or_not_owner' || error.message === 'user_not_found' ? 404 : 500;
res.status(status).json({ error: error.message });
}
}
}
export default ChatController;

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

@@ -26,51 +26,45 @@ class FalukantController {
}, { successStatus: 201 });
this.getInfo = this._wrapWithUser((userId) => this.service.getInfo(userId));
// Dashboard widget: originaler Endpoint (siehe Commit 62d8cd7)
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getBranches = this._wrapWithUser((userId) => this.service.getBranches(userId));
this.createBranch = this._wrapWithUser((userId, req) => this.service.createBranch(userId, req.body.cityId, req.body.branchTypeId), { 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.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) => {
const { branchId, productId, quantity } = req.body;
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.getStock = this._wrapWithUser((userId, req) => this.service.getStock(userId, req.params.branchId || null));
this.createStock = this._wrapWithUser((userId, req) => {
const { branchId, stockTypeId, stockSize } = req.body;
return this.service.createStock(userId, branchId, stockTypeId, stockSize);
}, { successStatus: 201, blockInDebtorsPrison: true });
}, { successStatus: 201 });
this.getProducts = this._wrapWithUser((userId) => this.service.getProducts(userId));
this.getInventory = this._wrapWithUser((userId, req) => this.service.getInventory(userId, req.params.branchId));
this.sellProduct = this._wrapWithUser((userId, req) => {
const { branchId, productId, quality, quantity } = req.body;
return this.service.sellProduct(userId, branchId, productId, quality, quantity);
}, { successStatus: 201, blockInDebtorsPrison: true });
}, { successStatus: 201 });
this.sellAllProducts = this._wrapWithUser((userId, req) => {
const { branchId } = req.body;
return this.service.sellAllProducts(userId, branchId);
}, { successStatus: 201, blockInDebtorsPrison: true });
}, { successStatus: 201 });
this.moneyHistory = this._wrapWithUser((userId, req) => {
let { page, filter } = req.body;
if (!page) page = 1;
return this.service.moneyHistory(userId, page, filter);
});
this.moneyHistoryGraph = this._wrapWithUser((userId, req) => {
const { range } = req.body || {};
return this.service.moneyHistoryGraph(userId, range || '24h');
});
this.getStorage = this._wrapWithUser((userId, req) => this.service.getStorage(userId, req.params.branchId));
this.buyStorage = this._wrapWithUser((userId, req) => {
const { branchId, amount, stockTypeId } = req.body;
return this.service.buyStorage(userId, branchId, amount, stockTypeId);
}, { successStatus: 201, blockInDebtorsPrison: true });
}, { successStatus: 201 });
this.sellStorage = this._wrapWithUser((userId, req) => {
const { branchId, amount, stockTypeId } = req.body;
return this.service.sellStorage(userId, branchId, amount, stockTypeId);
}, { successStatus: 202, blockInDebtorsPrison: true });
}, { successStatus: 202 });
this.getStockTypes = this._wrapSimple(() => this.service.getStockTypes());
this.getStockOverview = this._wrapSimple(() => this.service.getStockOverview());
@@ -80,137 +74,83 @@ class FalukantController {
console.log('🔍 getDirectorProposals called with userId:', userId, 'branchId:', req.body.branchId);
return this.service.getDirectorProposals(userId, req.body.branchId);
});
this.convertProposalToDirector = this._wrapWithUser((userId, req) => this.service.convertProposalToDirector(userId, req.body.proposalId), { 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.getAllDirectors = this._wrapWithUser((userId) => this.service.getAllDirectors(userId));
this.updateDirector = this._wrapWithUser((userId, req) => {
const { directorId, income } = req.body;
return this.service.updateDirector(userId, directorId, income);
}, { blockInDebtorsPrison: true });
});
this.setSetting = this._wrapWithUser((userId, req) => {
const { branchId, directorId, settingKey, value } = req.body;
return this.service.setSetting(userId, branchId, directorId, settingKey, value);
}, { blockInDebtorsPrison: true });
});
this.getFamily = this._wrapWithUser(async (userId) => {
const result = await this.service.getFamily(userId);
if (!result) throw { status: 404, message: 'No family data found' };
return result;
});
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId));
this.getPotentialHeirs = this._wrapWithUser((userId) => this.service.getPotentialHeirs(userId));
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId), { blockInDebtorsPrison: true });
this.setHeir = this._wrapWithUser((userId, req) => this.service.setHeir(userId, req.body.childCharacterId), { blockInDebtorsPrison: true });
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId), { blockInDebtorsPrison: true });
this.cancelWooing = this._wrapWithUser(async (userId) => {
try {
return await this.service.cancelWooing(userId);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'cancelTooSoon') {
throw { status: 412, message: 'cancelTooSoon', retryAt: e.meta?.retryAt };
}
throw e;
}
}, { successStatus: 202, blockInDebtorsPrison: true });
this.selectHeir = this._wrapWithUser((userId, req) => this.service.selectHeir(userId, req.body.heirId));
this.acceptMarriageProposal = this._wrapWithUser((userId, req) => this.service.acceptMarriageProposal(userId, req.body.proposalId));
this.getGifts = this._wrapWithUser((userId) => {
console.log('🔍 getGifts called with userId:', userId);
return this.service.getGifts(userId);
});
this.setLoverMaintenance = this._wrapWithUser((userId, req) =>
this.service.setLoverMaintenance(userId, req.params.relationshipId, req.body?.maintenanceLevel), { blockInDebtorsPrison: true });
this.createLoverRelationship = this._wrapWithUser((userId, req) =>
this.service.createLoverRelationship(userId, req.body?.targetCharacterId, req.body?.loverRole), { successStatus: 201, blockInDebtorsPrison: true });
this.spendTimeWithSpouse = this._wrapWithUser((userId) =>
this.service.spendTimeWithSpouse(userId), { blockInDebtorsPrison: true });
this.giftToSpouse = this._wrapWithUser((userId, req) =>
this.service.giftToSpouse(userId, req.body?.giftLevel), { blockInDebtorsPrison: true });
this.reconcileMarriage = this._wrapWithUser((userId) =>
this.service.reconcileMarriage(userId), { blockInDebtorsPrison: true });
this.acknowledgeLover = this._wrapWithUser((userId, req) =>
this.service.acknowledgeLover(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
this.endLoverRelationship = this._wrapWithUser((userId, req) =>
this.service.endLoverRelationship(userId, req.params.relationshipId), { blockInDebtorsPrison: true });
this.getChildren = this._wrapWithUser((userId) => this.service.getChildren(userId));
this.sendGift = this._wrapWithUser(async (userId, req) => {
try {
return await this.service.sendGift(userId, req.body.giftId);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooOften') {
throw { status: 412, message: 'tooOften', retryAt: e.meta?.retryAt };
}
throw e;
}
}, { blockInDebtorsPrison: true });
this.sendGift = this._wrapWithUser((userId, req) => this.service.sendGift(userId, req.body.giftId));
this.getTitlesOfNobility = this._wrapWithUser((userId) => this.service.getTitlesOfNobility(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) =>
this.service.executeReputationAction(userId, req.body?.actionTypeId), { successStatus: 201, blockInDebtorsPrison: true });
this.getHouseTypes = this._wrapWithUser((userId) => this.service.getHouseTypes(userId));
this.getMoodAffect = this._wrapWithUser((userId) => this.service.getMoodAffect(userId));
this.getCharacterAffect = this._wrapWithUser((userId) => this.service.getCharacterAffect(userId));
this.getUserHouse = this._wrapWithUser((userId) => this.service.getUserHouse(userId));
this.getBuyableHouses = this._wrapWithUser((userId) => this.service.getBuyableHouses(userId));
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201, blockInDebtorsPrison: true });
this.hireServants = this._wrapWithUser((userId, req) => this.service.hireServants(userId, req.body?.amount), { successStatus: 201, blockInDebtorsPrison: true });
this.dismissServants = this._wrapWithUser((userId, req) => this.service.dismissServants(userId, req.body?.amount), { blockInDebtorsPrison: true });
this.setServantPayLevel = this._wrapWithUser((userId, req) => this.service.setServantPayLevel(userId, req.body?.payLevel), { blockInDebtorsPrison: true });
this.tidyHousehold = this._wrapWithUser((userId) => this.service.tidyHousehold(userId), { blockInDebtorsPrison: true });
this.buyUserHouse = this._wrapWithUser((userId, req) => this.service.buyUserHouse(userId, req.body.houseId), { successStatus: 201 });
this.getPartyTypes = this._wrapWithUser((userId) => this.service.getPartyTypes(userId));
this.createParty = this._wrapWithUser((userId, req) => {
const { partyTypeId, musicId, banquetteId, nobilityIds, servantRatio } = req.body;
return this.service.createParty(userId, partyTypeId, musicId, banquetteId, nobilityIds, servantRatio);
}, { successStatus: 201, blockInDebtorsPrison: true });
}, { successStatus: 201 });
this.getParties = this._wrapWithUser((userId) => this.service.getParties(userId));
this.getReputationActions = this._wrapWithUser((userId) => this.service.getReputationActions(userId));
this.executeReputationAction = this._wrapWithUser((userId, req) => {
const { actionTypeId } = req.body;
return this.service.executeReputationAction(userId, actionTypeId);
}, { successStatus: 201 });
this.getNotBaptisedChildren = this._wrapWithUser((userId) => this.service.getNotBaptisedChildren(userId));
this.baptise = this._wrapWithUser((userId, req) => {
const { characterId: childId, firstName } = req.body;
return this.service.baptise(userId, childId, firstName);
}, { blockInDebtorsPrison: true });
this.getChurchOverview = this._wrapWithUser((userId) => this.service.getChurchOverview(userId));
this.getAvailableChurchPositions = this._wrapWithUser((userId) => this.service.getAvailableChurchPositions(userId));
this.getSupervisedApplications = this._wrapWithUser((userId) => this.service.getSupervisedApplications(userId));
this.applyForChurchPosition = this._wrapWithUser((userId, req) => {
const { officeTypeId, regionId } = req.body;
return this.service.applyForChurchPosition(userId, officeTypeId, regionId);
}, { blockInDebtorsPrison: true });
this.decideOnChurchApplication = this._wrapWithUser((userId, req) => {
const { applicationId, decision } = req.body;
return this.service.decideOnChurchApplication(userId, applicationId, decision);
}, { blockInDebtorsPrison: true });
});
this.getEducation = this._wrapWithUser((userId) => this.service.getEducation(userId));
this.sendToSchool = this._wrapWithUser((userId, req) => {
const { item, student, studentId } = req.body;
return this.service.sendToSchool(userId, item, student, studentId);
}, { blockInDebtorsPrison: true });
});
this.getBankOverview = this._wrapWithUser((userId) => this.service.getBankOverview(userId));
this.getBankCredits = this._wrapWithUser((userId) => this.service.getBankCredits(userId));
this.takeBankCredits = this._wrapWithUser((userId, req) => this.service.takeBankCredits(userId, req.body.height), { 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.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.healthActivity = this._wrapWithUser(async (userId, req) => {
try {
return await this.service.healthActivity(userId, req.body.measureTr);
} catch (e) {
if (e && e.name === 'PreconditionError' && e.message === 'tooClose') {
throw { status: 412, message: 'tooClose', retryAt: e.meta?.retryAt };
}
throw e;
}
}, { blockInDebtorsPrison: true });
this.healthActivity = this._wrapWithUser((userId, req) => this.service.healthActivity(userId, req.body.measureTr));
this.getPoliticsOverview = this._wrapWithUser((userId) => this.service.getPoliticsOverview(userId));
this.getOpenPolitics = this._wrapWithUser((userId) => this.service.getOpenPolitics(userId));
this.getElections = this._wrapWithUser((userId) => this.service.getElections(userId));
this.vote = this._wrapWithUser((userId, req) => this.service.vote(userId, req.body.votes), { blockInDebtorsPrison: true });
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds), { 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));
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
this.getBranchTaxes = this._wrapWithUser((userId, req) => this.service.getBranchTaxes(userId, req.params.branchId));
@@ -222,13 +162,6 @@ class FalukantController {
}
return this.service.getProductPriceInRegion(userId, productId, regionId);
});
this.getAllProductPricesInRegion = this._wrapWithUser((userId, req) => {
const regionId = parseInt(req.query.regionId, 10);
if (Number.isNaN(regionId)) {
throw new Error('regionId is required');
}
return this.service.getAllProductPricesInRegion(userId, regionId);
});
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
const productId = parseInt(req.query.productId, 10);
const currentPrice = parseFloat(req.query.currentPrice);
@@ -238,26 +171,13 @@ class FalukantController {
}
return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId);
});
this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => {
const body = req.body || {};
const items = Array.isArray(body.items) ? body.items : [];
const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null;
const valid = items.map(i => ({
productId: parseInt(i.productId, 10),
currentPrice: parseFloat(i.currentPrice)
})).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice));
return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId);
});
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element), { blockInDebtorsPrison: true });
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId), { blockInDebtorsPrison: true });
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(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.getAllNotifications = this._wrapWithUser((userId, req) => this.service.getAllNotifications(userId, req.query.page, req.query.size));
this.markNotificationsShown = this._wrapWithUser((userId) => this.service.markNotificationsShown(userId), { successStatus: 202 });
this.getDashboardWidget = this._wrapWithUser((userId) => this.service.getDashboardWidget(userId));
this.getUndergroundTargets = this._wrapWithUser((userId) => this.service.getPoliticalOfficeHolders(userId));
this.searchUsers = this._wrapWithUser((userId, req) => {
@@ -278,7 +198,7 @@ class FalukantController {
throw { status: 400, message: 'goal is required for corrupt_politician' };
}
return this.service.createUndergroundActivity(userId, payload);
}, { successStatus: 201, blockInDebtorsPrison: true });
}, { successStatus: 201 });
this.getUndergroundAttacks = this._wrapWithUser((userId, req) => {
const direction = (req.query.direction || '').toLowerCase();
@@ -292,14 +212,14 @@ class FalukantController {
this.getVehicleTypes = this._wrapWithUser((userId) => this.service.getVehicleTypes(userId));
this.buyVehicles = this._wrapWithUser(
(userId, req) => this.service.buyVehicles(userId, req.body),
{ successStatus: 201, blockInDebtorsPrison: true }
{ successStatus: 201 }
);
this.getVehicles = this._wrapWithUser(
(userId, req) => this.service.getVehicles(userId, req.query.regionId)
);
this.createTransport = this._wrapWithUser(
(userId, req) => this.service.createTransport(userId, req.body),
{ successStatus: 201, blockInDebtorsPrison: true }
{ successStatus: 201 }
);
this.getTransportRoute = this._wrapWithUser(
(userId, req) => this.service.getTransportRoute(userId, req.query)
@@ -309,40 +229,31 @@ class FalukantController {
);
this.repairVehicle = this._wrapWithUser(
(userId, req) => this.service.repairVehicle(userId, req.params.vehicleId),
{ successStatus: 200, blockInDebtorsPrison: true }
{ successStatus: 200 }
);
this.repairAllVehicles = this._wrapWithUser(
(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) => {
try {
const hashedUserId = extractHashedUserId(req);
if (!hashedUserId) {
return res.status(400).json({ error: 'Missing user identifier' });
}
if (blockInDebtorsPrison) {
await this.service.assertActionAllowedOutsideDebtorsPrison(hashedUserId);
}
const result = await fn(hashedUserId, req, res);
const toSend = postProcess ? postProcess(result) : result;
res.status(successStatus).json(toSend);
} catch (error) {
console.error('Controller error:', error);
const status = error.status && typeof error.status === 'number' ? error.status : 500;
// 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' });
}
}
};
}

View File

@@ -50,6 +50,11 @@ const menuStructure = {
visible: ["all"],
path: "/socialnetwork/gallery"
},
vocabtrainer: {
visible: ["all"],
path: "/socialnetwork/vocab",
children: {}
},
blockedUsers: {
visible: ["all"],
path: "/socialnetwork/blocked"
@@ -96,7 +101,9 @@ const menuStructure = {
},
eroticChat: {
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: {
visible: ["all"],
icon: "settings16.png",
@@ -212,10 +195,6 @@ const menuStructure = {
visible: ["all"],
path: "/settings/account"
},
languageAssistant: {
visible: ["all"],
path: "/settings/language-assistant"
},
personal: {
visible: ["all"],
path: "/settings/personal"
@@ -256,14 +235,6 @@ const menuStructure = {
visible: ["mainadmin", "useradministration"],
path: "/admin/users"
},
adultverification: {
visible: ["mainadmin", "useradministration"],
path: "/admin/users/adult-verification"
},
eroticmoderation: {
visible: ["mainadmin", "useradministration"],
path: "/admin/users/erotic-moderation"
},
userstatistics: {
visible: ["mainadmin"],
path: "/admin/users/statistics"
@@ -349,14 +320,7 @@ class NavigationController {
return age;
}
normalizeAdultVerificationStatus(value) {
if (['pending', 'approved', 'rejected'].includes(value)) {
return value;
}
return 'none';
}
async filterMenu(menu, rights, age, userId, adultVerificationStatus = 'none') {
async filterMenu(menu, rights, age, userId) {
const filteredMenu = {};
try {
const hasFalukantAccount = await this.hasFalukantAccount(userId);
@@ -370,17 +334,8 @@ class NavigationController {
|| (value.visible.includes('hasfalukantaccount') && hasFalukantAccount)) {
const { visible, ...itemWithoutVisible } = value;
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) {
filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId, adultVerificationStatus);
filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId);
}
}
}
@@ -407,33 +362,37 @@ class NavigationController {
required: false
}]
});
const userParams = await UserParam.findAll({
const userBirthdateParams = await UserParam.findAll({
where: { userId: user.id },
include: [
{
model: UserParamType,
as: 'paramType',
where: { description: ['birthdate', 'adult_verification_status'] }
where: { description: 'birthdate' }
}
]
});
let birthDate = (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 birthDate = userBirthdateParams.length > 0 ? userBirthdateParams[0].value : (new Date()).toDateString();
const age = this.calculateAge(birthDate);
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id, adultVerificationStatus);
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
// Vokabeltrainer: Sprachen werden im Frontend dynamisch geladen (wie Forum)
// Keine children mehr, da das Menü nur 2 Ebenen unterstützt
// Das Frontend lädt die Sprachen separat und zeigt sie als submenu2 an
// Dynamisches Submenü: Treffpunkt → Vokabeltrainer → (Neue Sprache + abonnierte/angelegte)
// Wichtig: "Neue Sprache" soll IMMER sichtbar sein auch wenn die DB-Abfrage (noch) fehlschlägt.
if (filteredMenu?.socialnetwork?.children?.vocabtrainer) {
const children = {
newLanguage: { path: '/socialnetwork/vocab/new' },
};
try {
const langs = await this.vocabService.listLanguagesForMenu(user.id);
for (const l of langs) {
children[`lang_${l.id}`] = { path: `/socialnetwork/vocab/${l.id}`, label: l.name };
}
} catch (e) {
console.warn('[menu] Konnte Vokabeltrainer-Sprachen nicht laden:', e?.message || e);
}
filteredMenu.socialnetwork.children.vocabtrainer.children = children;
}
res.status(200).json(filteredMenu);
} 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' });
}
}
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;

View File

@@ -15,19 +15,6 @@ class SocialNetworkController {
this.changeImage = this.changeImage.bind(this);
this.getFoldersByUsername = this.getFoldersByUsername.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.getGuestbookEntries = this.getGuestbookEntries.bind(this);
this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this);
@@ -160,8 +147,8 @@ class SocialNetworkController {
try {
const userId = req.headers.userid;
const { imageId } = req.params;
const { title, visibilities, selectedUsers } = req.body;
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities, selectedUsers);
const { title, visibilities } = req.body;
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities);
console.log('--->', folderId);
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
} 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) {
try {
const { htmlContent, recipientName } = req.body;

View File

@@ -9,7 +9,6 @@ class VocabController {
this.service = new VocabService();
this.listLanguages = this._wrapWithUser((userId) => this.service.listLanguages(userId));
this.listAllLanguages = this._wrapWithUser(() => this.service.listAllLanguages());
this.createLanguage = this._wrapWithUser((userId, req) => this.service.createLanguage(userId, req.body), { successStatus: 201 });
this.subscribe = this._wrapWithUser((userId, req) => this.service.subscribeByShareCode(userId, req.body), { successStatus: 201 });
this.getLanguage = this._wrapWithUser((userId, req) => this.service.getLanguage(userId, req.params.languageId));
@@ -22,40 +21,6 @@ class VocabController {
this.getChapter = this._wrapWithUser((userId, req) => this.service.getChapter(userId, req.params.chapterId));
this.listChapterVocabs = this._wrapWithUser((userId, req) => this.service.listChapterVocabs(userId, req.params.chapterId));
this.addVocabToChapter = this._wrapWithUser((userId, req) => this.service.addVocabToChapter(userId, req.params.chapterId, req.body), { successStatus: 201 });
// Courses
this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 });
this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query));
this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId));
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
// Lessons
this.getLesson = this._wrapWithUser((userId, req) => this.service.getLesson(userId, req.params.lessonId));
this.addLessonToCourse = this._wrapWithUser((userId, req) => this.service.addLessonToCourse(userId, req.params.courseId, req.body), { successStatus: 201 });
this.updateLesson = this._wrapWithUser((userId, req) => this.service.updateLesson(userId, req.params.lessonId, req.body));
this.deleteLesson = this._wrapWithUser((userId, req) => this.service.deleteLesson(userId, req.params.lessonId));
// Enrollment
this.enrollInCourse = this._wrapWithUser((userId, req) => this.service.enrollInCourse(userId, req.params.courseId), { successStatus: 201 });
this.unenrollFromCourse = this._wrapWithUser((userId, req) => this.service.unenrollFromCourse(userId, req.params.courseId));
this.getMyCourses = this._wrapWithUser((userId) => this.service.getMyCourses(userId));
// Progress
this.getCourseProgress = this._wrapWithUser((userId, req) => this.service.getCourseProgress(userId, req.params.courseId));
this.updateLessonProgress = this._wrapWithUser((userId, req) => this.service.updateLessonProgress(userId, req.params.lessonId, req.body));
// Grammar Exercises
this.getExerciseTypes = this._wrapWithUser((userId) => this.service.getExerciseTypes());
this.createGrammarExercise = this._wrapWithUser((userId, req) => this.service.createGrammarExercise(userId, req.params.lessonId, req.body), { successStatus: 201 });
this.getGrammarExercisesForLesson = this._wrapWithUser((userId, req) => this.service.getGrammarExercisesForLesson(userId, req.params.lessonId));
this.getGrammarExercise = this._wrapWithUser((userId, req) => this.service.getGrammarExercise(userId, req.params.exerciseId));
this.checkGrammarExerciseAnswer = this._wrapWithUser((userId, req) => this.service.checkGrammarExerciseAnswer(userId, req.params.exerciseId, req.body.answer));
this.getGrammarExerciseProgress = this._wrapWithUser((userId, req) => this.service.getGrammarExerciseProgress(userId, req.params.lessonId));
this.updateGrammarExercise = this._wrapWithUser((userId, req) => this.service.updateGrammarExercise(userId, req.params.exerciseId, req.body));
this.deleteGrammarExercise = this._wrapWithUser((userId, req) => this.service.deleteGrammarExercise(userId, req.params.exerciseId));
this.sendLessonAssistantMessage = this._wrapWithUser((userId, req) => this.service.sendLessonAssistantMessage(userId, req.params.lessonId, req.body), { successStatus: 201 });
}
_wrapWithUser(fn, { successStatus = 200 } = {}) {
@@ -78,3 +43,4 @@ class 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,
});
wss = new WebSocketServer({ server: httpsServer });
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
httpsServer.listen(PORT, '0.0.0.0', () => {
console.log(`[Daemon] WSS (TLS) Server gestartet auf Port ${PORT}`);
});
} else {
// Direkte Verbindung: lausche auf allen Interfaces (0.0.0.0)
wss = new WebSocketServer({ port: PORT, host: '0.0.0.0' });
wss = new WebSocketServer({ 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,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,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,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 User from './community/user.js';
import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js';
import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js';
import UserRight from './community/user_right.js';
@@ -18,15 +17,11 @@ import UserParamVisibilityType from './type/user_param_visibility.js';
import UserParamVisibility from './community/user_param_visibility.js';
import Folder from './community/folder.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 ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.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 Forum from './forum/forum.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 ProductType from './falukant/type/product.js';
import TitleOfNobility from './falukant/type/title_of_nobility.js';
import TitleBenefit from './falukant/type/title_benefit.js';
import TitleRequirement from './falukant/type/title_requirement.js';
import Branch from './falukant/data/branch.js';
import BranchType from './falukant/type/branch.js';
@@ -72,7 +66,6 @@ import PromotionalGiftCharacterTrait from './falukant/predefine/promotional_gift
import PromotionalGiftMood from './falukant/predefine/promotional_gift_mood.js';
import RelationshipType from './falukant/type/relationship.js';
import Relationship from './falukant/data/relationship.js';
import RelationshipState from './falukant/data/relationship_state.js';
import PromotionalGiftLog from './falukant/log/promotional_gift.js';
import HouseType from './falukant/type/house.js';
import BuyableHouse from './falukant/data/buyable_house.js';
@@ -100,10 +93,6 @@ import PoliticalOfficeRequirement from './falukant/predefine/political_office_pr
import PoliticalOfficePrerequisite from './falukant/predefine/political_office_prerequisite.js';
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js';
import ChurchOfficeType from './falukant/type/church_office_type.js';
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
import ChurchOffice from './falukant/data/church_office.js';
import ChurchApplication from './falukant/data/church_application.js';
import Underground from './falukant/data/underground.js';
import UndergroundType from './falukant/type/underground.js';
import VehicleType from './falukant/type/vehicle.js';
@@ -113,17 +102,8 @@ import RegionDistance from './falukant/data/region_distance.js';
import WeatherType from './falukant/type/weather.js';
import Weather from './falukant/data/weather.js';
import ProductWeatherEffect from './falukant/type/product_weather_effect.js';
import ProductPriceHistory from './falukant/log/product_price_history.js';
import Blog from './community/blog.js';
import BlogPost from './community/blog_post.js';
import VocabCourse from './community/vocab_course.js';
import VocabCourseLesson from './community/vocab_course_lesson.js';
import VocabCourseEnrollment from './community/vocab_course_enrollment.js';
import VocabCourseProgress from './community/vocab_course_progress.js';
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
import CalendarEvent from './community/calendar_event.js';
import Campaign from './match3/campaign.js';
import Match3Level from './match3/level.js';
import Objective from './match3/objective.js';
@@ -175,9 +155,6 @@ export default function setupAssociations() {
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasOne(UserDashboard, { foreignKey: 'userId', as: 'dashboard' });
UserDashboard.belongsTo(User, { foreignKey: 'userId', as: 'user' });
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });
@@ -213,14 +190,6 @@ export default function setupAssociations() {
Image.belongsTo(User, { 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
Folder.belongsToMany(ImageVisibilityType, {
through: FolderImageVisibility,
@@ -244,17 +213,6 @@ export default function setupAssociations() {
otherKey: 'imageId'
});
EroticVideo.belongsToMany(ImageVisibilityType, {
through: EroticVideoImageVisibility,
foreignKey: 'eroticVideoId',
otherKey: 'visibilityTypeId'
});
ImageVisibilityType.belongsToMany(EroticVideo, {
through: EroticVideoImageVisibility,
foreignKey: 'visibilityTypeId',
otherKey: 'eroticVideoId'
});
Folder.belongsToMany(ImageVisibilityUser, {
through: FolderVisibilityUser,
foreignKey: 'folderId',
@@ -266,19 +224,6 @@ export default function setupAssociations() {
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
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
@@ -379,8 +324,6 @@ export default function setupAssociations() {
FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' });
FalukantCharacter.belongsTo(FalukantCharacter, { foreignKey: 'pregnancyFatherCharacterId', as: 'pregnancyFather' });
FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' });
FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' });
@@ -392,8 +335,6 @@ export default function setupAssociations() {
TitleRequirement.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
TitleOfNobility.hasMany(TitleRequirement, { foreignKey: 'titleId', as: 'requirements' });
TitleOfNobility.hasMany(TitleBenefit, { foreignKey: 'titleId', as: 'benefits' });
TitleBenefit.belongsTo(TitleOfNobility, { foreignKey: 'titleId', as: 'title' });
Branch.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(Branch, { foreignKey: 'regionId', as: 'branches' });
@@ -464,13 +405,6 @@ export default function setupAssociations() {
DaySell.belongsTo(FalukantUser, { foreignKey: 'sellerId', as: 'user' });
FalukantUser.hasMany(DaySell, { foreignKey: 'sellerId', as: 'daySells' });
// Produkt-Preishistorie (Zeitreihe für Preiskurven)
ProductPriceHistory.belongsTo(ProductType, { foreignKey: 'productId', as: 'productType' });
ProductType.hasMany(ProductPriceHistory, { foreignKey: 'productId', as: 'priceHistory' });
ProductPriceHistory.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' });
RegionData.hasMany(ProductPriceHistory, { foreignKey: 'regionId', as: 'productPriceHistory' });
Notification.belongsTo(FalukantUser, { foreignKey: 'userId', as: 'user' });
FalukantUser.hasMany(Notification, { foreignKey: 'userId', as: 'notifications' });
@@ -499,8 +433,6 @@ export default function setupAssociations() {
Relationship.belongsTo(FalukantCharacter, { foreignKey: 'character2Id', as: 'character2', });
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character1Id', as: 'relationshipsAsCharacter1', });
FalukantCharacter.hasMany(Relationship, { foreignKey: 'character2Id', as: 'relationshipsAsCharacter2', });
Relationship.hasOne(RelationshipState, { foreignKey: 'relationshipId', as: 'state' });
RelationshipState.belongsTo(Relationship, { foreignKey: 'relationshipId', as: 'relationship' });
PromotionalGiftLog.belongsTo(PromotionalGift, { foreignKey: 'giftId', as: 'gift' });
PromotionalGift.hasMany(PromotionalGiftLog, { foreignKey: 'giftId', as: 'logs' });
@@ -626,14 +558,14 @@ export default function setupAssociations() {
Party.belongsToMany(TitleOfNobility, {
through: PartyInvitedNobility,
foreignKey: 'partyId',
otherKey: 'titleOfNobilityId',
foreignKey: 'party_id',
otherKey: 'title_of_nobility_id',
as: 'invitedNobilities',
});
TitleOfNobility.belongsToMany(Party, {
through: PartyInvitedNobility,
foreignKey: 'titleOfNobilityId',
otherKey: 'partyId',
foreignKey: 'title_of_nobility_id',
otherKey: 'party_id',
as: 'partiesInvitedTo',
});
@@ -927,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, {
foreignKey: 'undergroundTypeId',
as: 'undergroundType'
@@ -1099,40 +941,5 @@ export default function setupAssociations() {
TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' });
TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' });
// Vocab Course associations
VocabCourse.belongsTo(User, { foreignKey: 'ownerUserId', as: 'owner' });
User.hasMany(VocabCourse, { foreignKey: 'ownerUserId', as: 'ownedCourses' });
VocabCourseLesson.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseLesson, { foreignKey: 'courseId', as: 'lessons' });
VocabCourseEnrollment.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabCourseEnrollment, { foreignKey: 'userId', as: 'courseEnrollments' });
VocabCourseEnrollment.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseEnrollment, { foreignKey: 'courseId', as: 'enrollments' });
VocabCourseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabCourseProgress, { foreignKey: 'userId', as: 'courseProgress' });
VocabCourseProgress.belongsTo(VocabCourse, { foreignKey: 'courseId', as: 'course' });
VocabCourse.hasMany(VocabCourseProgress, { foreignKey: 'courseId', as: 'progress' });
VocabCourseProgress.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabCourseProgress, { foreignKey: 'lessonId', as: 'progress' });
// Grammar Exercise associations
VocabGrammarExercise.belongsTo(VocabCourseLesson, { foreignKey: 'lessonId', as: 'lesson' });
VocabCourseLesson.hasMany(VocabGrammarExercise, { foreignKey: 'lessonId', as: 'grammarExercises' });
VocabGrammarExercise.belongsTo(VocabGrammarExerciseType, { foreignKey: 'exerciseTypeId', as: 'exerciseType' });
VocabGrammarExerciseType.hasMany(VocabGrammarExercise, { foreignKey: 'exerciseTypeId', as: 'exercises' });
VocabGrammarExercise.belongsTo(User, { foreignKey: 'createdByUserId', as: 'creator' });
User.hasMany(VocabGrammarExercise, { foreignKey: 'createdByUserId', as: 'createdGrammarExercises' });
VocabGrammarExerciseProgress.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
// Calendar associations
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(CalendarEvent, { foreignKey: 'userId', as: 'calendarEvents' });
}

View File

@@ -20,10 +20,6 @@ const Room = sequelize.define('Room', {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true},
isAdultOnly: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false},
genderRestrictionId: {
type: DataTypes.INTEGER,
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: {
type: DataTypes.STRING,
allowNull: false},
isAdultArea: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
parentId: {
type: DataTypes.INTEGER,
allowNull: true

View File

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

View File

@@ -3,44 +3,6 @@ import { DataTypes } from 'sequelize';
import { encrypt, decrypt } from '../../utils/encryption.js';
import crypto from 'crypto';
function encodeEncryptedValueToBlob(value) {
const encrypted = encrypt(value);
return Buffer.from(encrypted, 'utf8');
}
function decodeEncryptedBlob(value) {
if (!value) {
return null;
}
try {
const encryptedUtf8 = value.toString('utf8');
const decryptedUtf8 = decrypt(encryptedUtf8);
if (decryptedUtf8) {
return decryptedUtf8;
}
} catch (error) {
console.warn('Email utf8 decryption failed, trying legacy hex format:', error.message);
}
try {
const encryptedHex = value.toString('hex');
const decryptedHex = decrypt(encryptedHex);
if (decryptedHex) {
return decryptedHex;
}
} catch (error) {
console.warn('Email legacy hex decryption failed:', error.message);
}
try {
return value.toString('utf8');
} catch (error) {
console.warn('Email could not be read as plain text:', error.message);
return null;
}
}
const User = sequelize.define('user', {
email: {
type: DataTypes.BLOB,
@@ -48,12 +10,35 @@ const User = sequelize.define('user', {
unique: true,
set(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() {
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: {

View File

@@ -1,24 +0,0 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
import User from './user.js';
const UserDashboard = sequelize.define('user_dashboard', {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
references: { model: User, key: 'id' }
},
config: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: { widgets: [] }
}
}, {
tableName: 'user_dashboard',
schema: 'community',
underscored: true,
timestamps: false
});
export default UserDashboard;

View File

@@ -14,7 +14,7 @@ const UserParam = sequelize.define('user_param', {
allowNull: false
},
value: {
type: DataTypes.TEXT,
type: DataTypes.STRING,
allowNull: false,
set(value) {
if (value) {

View File

@@ -1,75 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourse extends Model {}
VocabCourse.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
ownerUserId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'owner_user_id'
},
title: {
type: DataTypes.TEXT,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
languageId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'language_id'
},
nativeLanguageId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'native_language_id',
comment: 'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".'
},
difficultyLevel: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
field: 'difficulty_level'
},
isPublic: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'is_public'
},
shareCode: {
type: DataTypes.TEXT,
allowNull: true,
unique: true,
field: 'share_code'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'updated_at'
}
}, {
sequelize,
modelName: 'VocabCourse',
tableName: 'vocab_course',
schema: 'community',
timestamps: true,
underscored: true
});
export default VocabCourse;

View File

@@ -1,37 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourseEnrollment extends Model {}
VocabCourseEnrollment.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'user_id'
},
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'course_id'
},
enrolledAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'enrolled_at'
}
}, {
sequelize,
modelName: 'VocabCourseEnrollment',
tableName: 'vocab_course_enrollment',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabCourseEnrollment;

View File

@@ -1,118 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourseLesson extends Model {}
VocabCourseLesson.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'course_id'
},
chapterId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'chapter_id'
},
lessonNumber: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'lesson_number'
},
title: {
type: DataTypes.TEXT,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
weekNumber: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'week_number'
},
dayNumber: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'day_number'
},
lessonType: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: 'vocab',
field: 'lesson_type'
},
audioUrl: {
type: DataTypes.TEXT,
allowNull: true,
field: 'audio_url'
},
culturalNotes: {
type: DataTypes.TEXT,
allowNull: true,
field: 'cultural_notes'
},
learningGoals: {
type: DataTypes.JSONB,
allowNull: true,
field: 'learning_goals'
},
corePatterns: {
type: DataTypes.JSONB,
allowNull: true,
field: 'core_patterns'
},
grammarFocus: {
type: DataTypes.JSONB,
allowNull: true,
field: 'grammar_focus'
},
speakingPrompts: {
type: DataTypes.JSONB,
allowNull: true,
field: 'speaking_prompts'
},
practicalTasks: {
type: DataTypes.JSONB,
allowNull: true,
field: 'practical_tasks'
},
targetMinutes: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'target_minutes'
},
targetScorePercent: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 80,
field: 'target_score_percent'
},
requiresReview: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'requires_review'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
}
}, {
sequelize,
modelName: 'VocabCourseLesson',
tableName: 'vocab_course_lesson',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabCourseLesson;

View File

@@ -1,56 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabCourseProgress extends Model {}
VocabCourseProgress.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'user_id'
},
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'course_id'
},
lessonId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'lesson_id'
},
completed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
score: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
lastAccessedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_accessed_at'
},
completedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'completed_at'
}
}, {
sequelize,
modelName: 'VocabCourseProgress',
tableName: 'vocab_course_progress',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabCourseProgress;

View File

@@ -1,69 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabGrammarExercise extends Model {}
VocabGrammarExercise.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
lessonId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'lesson_id'
},
exerciseTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'exercise_type_id'
},
exerciseNumber: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'exercise_number'
},
title: {
type: DataTypes.TEXT,
allowNull: false
},
instruction: {
type: DataTypes.TEXT,
allowNull: true
},
questionData: {
type: DataTypes.JSONB,
allowNull: false,
field: 'question_data'
},
answerData: {
type: DataTypes.JSONB,
allowNull: false,
field: 'answer_data'
},
explanation: {
type: DataTypes.TEXT,
allowNull: true
},
createdByUserId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'created_by_user_id'
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
}
}, {
sequelize,
modelName: 'VocabGrammarExercise',
tableName: 'vocab_grammar_exercise',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabGrammarExercise;

View File

@@ -1,57 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabGrammarExerciseProgress extends Model {}
VocabGrammarExerciseProgress.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'user_id'
},
exerciseId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'exercise_id'
},
attempts: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
correctAttempts: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'correct_attempts'
},
lastAttemptAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_attempt_at'
},
completed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
completedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'completed_at'
}
}, {
sequelize,
modelName: 'VocabGrammarExerciseProgress',
tableName: 'vocab_grammar_exercise_progress',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabGrammarExerciseProgress;

View File

@@ -1,36 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class VocabGrammarExerciseType extends Model {}
VocabGrammarExerciseType.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.TEXT,
allowNull: false,
unique: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: 'created_at'
}
}, {
sequelize,
modelName: 'VocabGrammarExerciseType',
tableName: 'vocab_grammar_exercise_type',
schema: 'community',
timestamps: false,
underscored: true
});
export default VocabGrammarExerciseType;

View File

@@ -45,14 +45,6 @@ FalukantCharacter.init(
min: 0,
max: 100
}
},
pregnancyDueAt: {
type: DataTypes.DATE,
allowNull: true,
},
pregnancyFatherCharacterId: {
type: DataTypes.INTEGER,
allowNull: true,
}
},
{
@@ -61,12 +53,7 @@ FalukantCharacter.init(
tableName: 'character',
schema: 'falukant_data',
timestamps: true,
underscored: true,
// Spalten erst nach Migration 20260330000000; ohne Exclude würde SELECT/INSERT auf alten DBs fehlschlagen
defaultScope: {
attributes: { exclude: ['pregnancyDueAt', 'pregnancyFatherCharacterId'] },
},
}
underscored: true}
);
export default FalukantCharacter;

View File

@@ -27,25 +27,7 @@ ChildRelation.init(
isHeir: {
type: DataTypes.BOOLEAN,
allowNull: true,
default: false},
legitimacy: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'legitimate',
validate: {
isIn: [['legitimate', 'acknowledged_bastard', 'hidden_bastard']]
}},
birthContext: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'marriage',
validate: {
isIn: [['marriage', 'lover']]
}},
publicKnown: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false}
default: false}
},
{
sequelize,

View File

@@ -1,47 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchApplication extends Model {}
ChurchApplication.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false
},
characterId: {
type: DataTypes.INTEGER,
allowNull: false
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false
},
supervisorId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'ID des Vorgesetzten, der über die Bewerbung entscheidet (null für Einstiegspositionen ohne Supervisor)'
},
status: {
type: DataTypes.ENUM('pending', 'approved', 'rejected'),
allowNull: false,
defaultValue: 'pending'
},
decisionDate: {
type: DataTypes.DATE,
allowNull: true
}
}, {
sequelize,
modelName: 'ChurchApplication',
tableName: 'church_application',
schema: 'falukant_data',
timestamps: true,
underscored: true
});
export default ChurchApplication;

View File

@@ -1,38 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchOffice extends Model {}
ChurchOffice.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false
},
characterId: {
type: DataTypes.INTEGER,
allowNull: false
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false
},
supervisorId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'ID des Vorgesetzten (höhere Position in der Hierarchie)'
}
}, {
sequelize,
modelName: 'ChurchOffice',
tableName: 'church_office',
schema: 'falukant_data',
timestamps: true,
underscored: true
});
export default ChurchOffice;

View File

@@ -7,57 +7,7 @@ DebtorsPrism.init({
// Verknüpfung auf FalukantCharacter
characterId: {
type: DataTypes.INTEGER,
allowNull: false
},
status: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'delinquent'
},
enteredAt: {
type: DataTypes.DATE,
allowNull: true
},
releasedAt: {
type: DataTypes.DATE,
allowNull: true
},
debtAtEntry: {
type: DataTypes.DECIMAL(14, 2),
allowNull: true
},
remainingDebt: {
type: DataTypes.DECIMAL(14, 2),
allowNull: true
},
daysOverdue: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
reason: {
type: DataTypes.STRING,
allowNull: true
},
creditworthinessPenalty: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
nextForcedAction: {
type: DataTypes.STRING,
allowNull: true
},
assetsSeizedJson: {
type: DataTypes.JSONB,
allowNull: true
},
publicKnown: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
}
}, {
allowNull: false}}, {
sequelize,
modelName: 'DebtorsPrism',
tableName: 'debtors_prism',

View File

@@ -22,12 +22,7 @@ Production.init({
startTimestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')},
sleep: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Produktion ist zurückgestellt'}
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')}
}, {
sequelize,
modelName: 'Production',

View File

@@ -10,20 +10,11 @@ RegionData.init({
allowNull: false},
regionTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: RegionType,
key: 'id',
schema: 'falukant_type'
}
allowNull: false
},
parentId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: 'region',
key: 'id',
schema: 'falukant_data'}
allowNull: true
},
map: {
type: DataTypes.JSONB,

View File

@@ -1,141 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class RelationshipState extends Model {}
RelationshipState.init(
{
relationshipId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
},
marriageSatisfaction: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 55,
validate: {
min: 0,
max: 100,
},
},
marriagePublicStability: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 55,
validate: {
min: 0,
max: 100,
},
},
loverRole: {
type: DataTypes.STRING,
allowNull: true,
validate: {
isIn: [[null, 'secret_affair', 'lover', 'mistress_or_favorite'].filter(Boolean)],
},
},
affection: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 50,
validate: {
min: 0,
max: 100,
},
},
visibility: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 15,
validate: {
min: 0,
max: 100,
},
},
discretion: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 50,
validate: {
min: 0,
max: 100,
},
},
maintenanceLevel: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 50,
validate: {
min: 0,
max: 100,
},
},
statusFit: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
validate: {
min: -2,
max: 2,
},
},
monthlyBaseCost: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
validate: {
min: 0,
},
},
monthsUnderfunded: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
validate: {
min: 0,
},
},
active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
acknowledged: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
exclusiveFlag: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
lastMonthlyProcessedAt: {
type: DataTypes.DATE,
allowNull: true,
},
lastDailyProcessedAt: {
type: DataTypes.DATE,
allowNull: true,
},
notesJson: {
type: DataTypes.JSONB,
allowNull: true,
},
flagsJson: {
type: DataTypes.JSONB,
allowNull: true,
},
},
{
sequelize,
modelName: 'RelationshipState',
tableName: 'relationship_state',
schema: 'falukant_data',
timestamps: true,
underscored: true,
}
);
export default RelationshipState;

View File

@@ -6,8 +6,7 @@ class FalukantStock extends Model { }
FalukantStock.init({
branchId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
allowNull: false
},
stockTypeId: {
type: DataTypes.INTEGER,

View File

@@ -22,9 +22,8 @@ TownProductWorth.init({
timestamps: false,
underscored: true,
hooks: {
// Neu: 5585 %; ältere Einträge können 4060 % sein (Preislogik im Service deckelt nach unten ab).
beforeCreate: (worthPercent) => {
worthPercent.worthPercent = Math.floor(Math.random() * 31) + 55;
worthPercent.worthPercent = Math.floor(Math.random() * 20) + 40;
}
}
});

View File

@@ -25,11 +25,6 @@ Transport.init(
type: DataTypes.INTEGER,
allowNull: false,
},
guardCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
},
{
sequelize,
@@ -43,3 +38,4 @@ Transport.init(
export default Transport;

View File

@@ -12,7 +12,7 @@ Underground.init({
allowNull: false},
victimId: {
type: DataTypes.INTEGER,
allowNull: true},
allowNull: false},
parameters: {
type: DataTypes.JSON,
allowNull: true},

View File

@@ -24,35 +24,6 @@ UserHouse.init({
allowNull: false,
defaultValue: 100
},
servantCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
servantQuality: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 50
},
servantPayLevel: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'normal'
},
householdOrder: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 55
},
householdTensionScore: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 10
},
householdTensionReasonsJson: {
type: DataTypes.JSONB,
allowNull: true
},
houseTypeId: {
type: DataTypes.INTEGER,
allowNull: false

View File

@@ -1,44 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
/**
* Preishistorie pro Produkt und Region (Zeitreihe für Preis-Graphen).
* Aktuell wird diese Tabelle noch nicht befüllt; sie dient nur als Grundlage.
*/
class ProductPriceHistory extends Model { }
ProductPriceHistory.init({
productId: {
type: DataTypes.INTEGER,
allowNull: false
},
regionId: {
type: DataTypes.INTEGER,
allowNull: false
},
price: {
type: DataTypes.DECIMAL(12, 2),
allowNull: false
},
recordedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
}
}, {
sequelize,
modelName: 'ProductPriceHistory',
tableName: 'product_price_history',
schema: 'falukant_log',
timestamps: false,
underscored: true,
indexes: [
{
name: 'product_price_history_product_region_recorded_idx',
fields: ['product_id', 'region_id', 'recorded_at']
}
]
});
export default ProductPriceHistory;

View File

@@ -1,49 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
/**
* Log aller Änderungen an relationship und marriage_proposals.
* Einträge werden ausschließlich durch DB-Trigger geschrieben und nicht gelöscht.
* Hilft zu analysieren, warum z.B. Werbungen um einen Partner verschwinden.
*/
class RelationshipChangeLog extends Model {}
RelationshipChangeLog.init(
{
changedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
tableName: {
type: DataTypes.STRING(64),
allowNull: false
},
operation: {
type: DataTypes.STRING(16),
allowNull: false
},
recordId: {
type: DataTypes.INTEGER,
allowNull: true
},
payloadOld: {
type: DataTypes.JSONB,
allowNull: true
},
payloadNew: {
type: DataTypes.JSONB,
allowNull: true
}
},
{
sequelize,
modelName: 'RelationshipChangeLog',
tableName: 'relationship_change_log',
schema: 'falukant_log',
timestamps: false,
underscored: true
}
);
export default RelationshipChangeLog;

View File

@@ -1,35 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchOfficeRequirement extends Model {}
ChurchOfficeRequirement.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
officeTypeId: {
type: DataTypes.INTEGER,
allowNull: false
},
prerequisiteOfficeTypeId: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Erforderliche niedrigere Position in der Hierarchie'
},
minTitleLevel: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Mindest-Titel-Level (optional)'
}
}, {
sequelize,
modelName: 'ChurchOfficeRequirement',
tableName: 'church_office_requirement',
schema: 'falukant_predefine',
timestamps: false,
underscored: true
});
export default ChurchOfficeRequirement;

View File

@@ -10,14 +10,12 @@ PromotionalGiftCharacterTrait.init(
giftId: {
type: DataTypes.INTEGER,
field: 'gift_id',
allowNull: false,
primaryKey: true
allowNull: false
},
traitId: {
type: DataTypes.INTEGER,
field: 'trait_id',
allowNull: false,
primaryKey: true
allowNull: false
},
suitability: {
type: DataTypes.INTEGER,

View File

@@ -10,14 +10,12 @@ PromotionalGiftMood.init(
giftId: {
type: DataTypes.INTEGER,
field: 'gift_id',
allowNull: false,
primaryKey: true
allowNull: false
},
moodId: {
type: DataTypes.INTEGER,
field: 'mood_id',
allowNull: false,
primaryKey: true
allowNull: false
},
suitability: {
type: DataTypes.INTEGER,

View File

@@ -1,38 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
class ChurchOfficeType extends Model {}
ChurchOfficeType.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
seatsPerRegion: {
type: DataTypes.INTEGER,
allowNull: false
},
regionType: {
type: DataTypes.STRING,
allowNull: false
},
hierarchyLevel: {
type: DataTypes.INTEGER,
allowNull: false,
comment: 'Höhere Zahl = höhere Position in der Hierarchie'
}
}, {
sequelize,
modelName: 'ChurchOfficeType',
tableName: 'church_office_type',
schema: 'falukant_type',
timestamps: false,
underscored: true
});
export default ChurchOfficeType;

View File

@@ -15,8 +15,7 @@ ProductType.init({
allowNull: false},
sellCost: {
type: DataTypes.INTEGER,
allowNull: false
}
allowNull: false}
}, {
sequelize,
modelName: 'ProductType',

View File

@@ -1,41 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../../utils/sequelize.js';
/**
* Vorteile pro Stand (Adelstitel).
* benefit_type: 'tax_share' | 'tax_exempt' | 'office_eligibility' | 'free_party_type' | 'reputation_bonus'
* parameters: JSONB, z.B. { officeTypeNames: [] }, { partyTypeIds: [] }, { minPercent: 5, maxPercent: 15 }
*/
class TitleBenefit extends Model {}
TitleBenefit.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
titleId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'title_id'
},
benefitType: {
type: DataTypes.STRING,
allowNull: false,
field: 'benefit_type'
},
parameters: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: {}
}
}, {
sequelize,
modelName: 'TitleBenefit',
tableName: 'title_benefit',
schema: 'falukant_type',
timestamps: false,
underscored: true
});
export default TitleBenefit;

View File

@@ -4,10 +4,8 @@ import SettingsType from './type/settings.js';
import UserParamValue from './type/user_param_value.js';
import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js';
import WidgetType from './type/widget_type.js';
import User from './community/user.js';
import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js';
import Login from './logs/login.js';
import UserRight from './community/user_right.js';
import InterestType from './type/interest.js';
@@ -18,15 +16,11 @@ import UserParamVisibilityType from './type/user_param_visibility.js';
import UserParamVisibility from './community/user_param_visibility.js';
import Folder from './community/folder.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 ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.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 DiaryHistory from './community/diary_history.js';
import Diary from './community/diary.js';
@@ -55,7 +49,6 @@ import ProductType from './falukant/type/product.js';
import Knowledge from './falukant/data/product_knowledge.js';
import TitleRequirement from './falukant/type/title_requirement.js';
import TitleOfNobility from './falukant/type/title_of_nobility.js';
import TitleBenefit from './falukant/type/title_benefit.js';
import BranchType from './falukant/type/branch.js';
import Branch from './falukant/data/branch.js';
import Production from './falukant/data/production.js';
@@ -71,7 +64,6 @@ import Notification from './falukant/log/notification.js';
import MarriageProposal from './falukant/data/marriage_proposal.js';
import RelationshipType from './falukant/type/relationship.js';
import Relationship from './falukant/data/relationship.js';
import RelationshipState from './falukant/data/relationship_state.js';
import CharacterTrait from './falukant/type/character_trait.js';
import FalukantCharacterTrait from './falukant/data/falukant_character_trait.js';
import Mood from './falukant/type/mood.js';
@@ -95,7 +87,6 @@ import Learning from './falukant/data/learning.js';
import Credit from './falukant/data/credit.js';
import DebtorsPrism from './falukant/data/debtors_prism.js';
import HealthActivity from './falukant/log/health_activity.js';
import ProductPriceHistory from './falukant/log/product_price_history.js';
// — Match3 Minigame —
import Match3Campaign from './match3/campaign.js';
@@ -122,13 +113,6 @@ import Vote from './falukant/data/vote.js';
import ElectionResult from './falukant/data/election_result.js';
import PoliticalOfficeHistory from './falukant/log/political_office_history.js';
import ElectionHistory from './falukant/log/election_history.js';
import RelationshipChangeLog from './falukant/log/relationship_change_log.js';
// — Kirchliche Ämter (Church) —
import ChurchOfficeType from './falukant/type/church_office_type.js';
import ChurchOfficeRequirement from './falukant/predefine/church_office_requirement.js';
import ChurchOffice from './falukant/data/church_office.js';
import ChurchApplication from './falukant/data/church_application.js';
import UndergroundType from './falukant/type/underground.js';
import Underground from './falukant/data/underground.js';
import VehicleType from './falukant/type/vehicle.js';
@@ -145,25 +129,13 @@ import ChatRight from './chat/rights.js';
import ChatUserRight from './chat/user_rights.js';
import RoomType from './chat/room_type.js';
// — Vocab Courses —
import VocabCourse from './community/vocab_course.js';
import VocabCourseLesson from './community/vocab_course_lesson.js';
import VocabCourseEnrollment from './community/vocab_course_enrollment.js';
import VocabCourseProgress from './community/vocab_course_progress.js';
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
import CalendarEvent from './community/calendar_event.js';
const models = {
SettingsType,
UserParamValue,
UserParamType,
UserRightType,
WidgetType,
User,
UserParam,
UserDashboard,
Login,
UserRight,
InterestType,
@@ -174,15 +146,11 @@ const models = {
UserParamVisibility,
Folder,
Image,
EroticVideo,
EroticContentReport,
ImageVisibilityType,
ImageVisibilityUser,
FolderImageVisibility,
ImageImageVisibility,
FolderVisibilityUser,
EroticVideoImageVisibility,
EroticVideoVisibilityUser,
GuestbookEntry,
DiaryHistory,
Diary,
@@ -211,7 +179,6 @@ const models = {
ProductType,
Knowledge,
TitleOfNobility,
TitleBenefit,
TitleRequirement,
BranchType,
Branch,
@@ -228,7 +195,6 @@ const models = {
MarriageProposal,
RelationshipType,
Relationship,
RelationshipState,
CharacterTrait,
FalukantCharacterTrait,
Mood,
@@ -252,7 +218,6 @@ const models = {
Credit,
DebtorsPrism,
HealthActivity,
ProductPriceHistory,
RegionDistance,
VehicleType,
Vehicle,
@@ -268,11 +233,6 @@ const models = {
ElectionResult,
PoliticalOfficeHistory,
ElectionHistory,
RelationshipChangeLog,
ChurchOfficeType,
ChurchOfficeRequirement,
ChurchOffice,
ChurchApplication,
UndergroundType,
Underground,
WeatherType,
@@ -303,18 +263,6 @@ const models = {
TaxiMapTileStreet,
TaxiMapTileHouse,
TaxiHighscore,
// Vocab Courses
VocabCourse,
VocabCourseLesson,
VocabCourseEnrollment,
VocabCourseProgress,
VocabGrammarExerciseType,
VocabGrammarExercise,
VocabGrammarExerciseProgress,
// Calendar
CalendarEvent,
};
export default models;

View File

@@ -9,13 +9,13 @@ const ContactMessage = sequelize.define('contact_message', {
set(value) {
if (value) {
const encryptedValue = encrypt(value);
this.setDataValue('email', encryptedValue);
this.setDataValue('email', encryptedValue.toString('hex'));
}
},
get() {
const value = this.getDataValue('email');
if (value) {
return decrypt(value);
return decrypt(Buffer.from(value, 'hex'));
}
}
},
@@ -25,13 +25,13 @@ const ContactMessage = sequelize.define('contact_message', {
set(value) {
if (value) {
const encryptedValue = encrypt(value);
this.setDataValue('message', encryptedValue);
this.setDataValue('message', encryptedValue.toString('hex'));
}
},
get() {
const value = this.getDataValue('message');
if (value) {
return decrypt(value);
return decrypt(Buffer.from(value, 'hex'));
}
}
},
@@ -41,13 +41,13 @@ const ContactMessage = sequelize.define('contact_message', {
set(value) {
if (value) {
const encryptedValue = encrypt(value);
this.setDataValue('name', encryptedValue);
this.setDataValue('name', encryptedValue.toString('hex'));
}
},
get() {
const value = this.getDataValue('name');
if (value) {
return decrypt(value);
return decrypt(Buffer.from(value, 'hex'));
}
}
},
@@ -67,13 +67,13 @@ const ContactMessage = sequelize.define('contact_message', {
set(value) {
if (value) {
const encryptedValue = encrypt(value);
this.setDataValue('answer', encryptedValue);
this.setDataValue('answer', encryptedValue.toString('hex'));
}
},
get() {
const value = this.getDataValue('answer');
if (value) {
return decrypt(value);
return decrypt(Buffer.from(value, 'hex'));
}
}
},

View File

@@ -350,16 +350,15 @@ export async function createTriggers() {
SELECT * FROM random_fill
),
-- 8) Neue Ämter anlegen created_at = Wahldatum (Amtsbeginn), nicht NOW()
-- damit termEnds = Amtsbeginn + termLength korrekt berechnet werden kann
-- 8) Neue Ämter anlegen und sofort zurückliefern
created_offices AS (
INSERT INTO falukant_data.political_office
(office_type_id, character_id, created_at, updated_at, region_id)
SELECT
tp.tp_office_type_id,
fw.character_id,
tp.tp_election_date AS created_at,
tp.tp_election_date AS updated_at,
NOW() AS created_at,
NOW() AS updated_at,
tp.tp_region_id
FROM final_winners fw
JOIN to_process tp

View File

@@ -1,39 +0,0 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const WidgetType = sequelize.define('widget_type', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
label: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Anzeigename des Widgets (z. B. "Termine")'
},
endpoint: {
type: DataTypes.STRING,
allowNull: false,
comment: 'API-Pfad (z. B. "/api/termine")'
},
description: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Optionale Beschreibung'
},
orderId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'order_id',
comment: 'Sortierreihenfolge'
}
}, {
tableName: 'widget_type',
schema: 'type',
underscored: true,
timestamps: false
});
export default WidgetType;

3221
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "backend",
"version": "1.0.0",
"description": "Nach Änderungen an dependencies: npm install ausführen und package-lock.json committen (npm ci im Deploy).",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
@@ -9,47 +9,37 @@
"dev": "NODE_ENV=development node server.js",
"start-daemon": "node daemonServer.js",
"sync-db": "node sync-database.js",
"sync-tables": "node sync-tables-only.js",
"check-connections": "node check-connections.js",
"cleanup-connections": "node cleanup-connections.js",
"diag:town-worth": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-town-product-worth-stats.mjs",
"diag:moneyflow": "QUIET_ENV_LOGS=1 DOTENV_CONFIG_QUIET=1 node scripts/falukant-moneyflow-report.mjs",
"lockfile:sync": "npm install --package-lock-only",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@gltf-transform/cli": "^4.3.0",
"amqplib": "^0.10.9",
"bcryptjs": "^3.0.3",
"connect-redis": "^9.0.0",
"cors": "^2.8.6",
"amqplib": "^0.10.4",
"bcryptjs": "^2.4.3",
"connect-redis": "^7.1.1",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"dompurify": "^3.3.3",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express-session": "^1.19.0",
"i18n": "^0.15.3",
"joi": "^18.0.2",
"jsdom": "^29.0.1",
"multer": "^2.1.1",
"mysql2": "^3.20.0",
"nodemailer": "^8.0.3",
"pg": "^8.20.0",
"dompurify": "^3.1.7",
"dotenv": "^17.2.1",
"express": "^4.19.2",
"express-session": "^1.18.1",
"i18n": "^0.15.1",
"joi": "^17.13.3",
"jsdom": "^26.1.0",
"multer": "^2.0.0",
"mysql2": "^3.10.3",
"nodemailer": "^7.0.11",
"pg": "^8.12.0",
"pg-hstore": "^2.3.4",
"redis": "^5.11.0",
"sequelize": "^6.37.8",
"sharp": "^0.34.5",
"socket.io": "^4.8.3",
"uuid": "^13.0.0",
"ws": "^8.20.0"
"redis": "^4.7.0",
"sequelize": "^6.37.3",
"sharp": "^0.34.3",
"socket.io": "^4.7.5",
"uuid": "^11.1.0",
"ws": "^8.18.0"
},
"devDependencies": {
"sequelize-cli": "^6.6.5"
},
"overrides": {
"minimatch": "10.2.4"
"sequelize-cli": "^6.6.2"
}
}

View File

@@ -19,12 +19,6 @@ router.delete('/chat/rooms/:id', authenticate, adminController.deleteRoom);
router.get('/users/search', authenticate, adminController.searchUsers);
router.get('/users/statistics', authenticate, adminController.getUserStatistics);
router.get('/users/batch', authenticate, adminController.getUsers);
router.get('/users/adult-verification', authenticate, adminController.getAdultVerificationRequests);
router.get('/users/:id/adult-verification/document', authenticate, adminController.getAdultVerificationDocument);
router.put('/users/:id/adult-verification', authenticate, adminController.setAdultVerificationStatus);
router.get('/users/erotic-moderation', authenticate, adminController.getEroticModerationReports);
router.get('/users/erotic-moderation/preview/:type/:targetId', authenticate, adminController.getEroticModerationPreview);
router.put('/users/erotic-moderation/:id', authenticate, adminController.applyEroticModerationAction);
router.get('/users/:id', authenticate, adminController.getUser);
router.put('/users/:id', authenticate, adminController.updateUser);
@@ -43,9 +37,6 @@ router.post('/contacts/answer', authenticate, adminController.answerContact);
router.post('/falukant/searchuser', authenticate, adminController.searchUser);
router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById);
router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser);
router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy);
router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth);
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);

View File

@@ -1,20 +0,0 @@
import { Router } from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import calendarController from '../controllers/calendarController.js';
const router = Router();
// All routes require authentication
router.get('/events', authenticate, calendarController.getEvents);
router.get('/events/:id', authenticate, calendarController.getEvent);
router.post('/events', authenticate, calendarController.createEvent);
router.put('/events/:id', authenticate, calendarController.updateEvent);
router.delete('/events/:id', authenticate, calendarController.deleteEvent);
router.get('/birthdays', authenticate, calendarController.getFriendsBirthdays);
// Widget endpoints
router.get('/widget/birthdays', authenticate, calendarController.getWidgetBirthdays);
router.get('/widget/upcoming', authenticate, calendarController.getWidgetUpcoming);
router.get('/widget/mini', authenticate, calendarController.getWidgetMiniCalendar);
export default router;

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