Feedacksystem hinzugefügt, Bugs in der Timelock behoben

This commit is contained in:
2026-03-25 16:29:50 +01:00
parent 528ea89bc4
commit eb741daf4c
90 changed files with 4670 additions and 2146 deletions

View File

@@ -39,7 +39,8 @@
"Bash(head -15 grep -n -B5 -A2 \"max-width: 480px\\\\|max-width:480px\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/joinlock.html)", "Bash(head -15 grep -n -B5 -A2 \"max-width: 480px\\\\|max-width:480px\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/joinlock.html)",
"Bash(stat /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/*)", "Bash(stat /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/*)",
"Bash(git -C /home/mario/Workspaces/xxx-thegame diff HEAD xxxthegame/src/main/resources/static/neulock.html)", "Bash(git -C /home/mario/Workspaces/xxx-thegame diff HEAD xxxthegame/src/main/resources/static/neulock.html)",
"Bash(1:*)" "Bash(1:*)",
"Bash(python3:*)"
] ]
} }
} }

View File

@@ -1,5 +1,5 @@
#Tue Mar 24 11:25:59 CET 2026 #Wed Mar 25 07:26:10 CET 2026
display=\:0 display=\:0
host=mario-mint host=mario-mint
process-id=2972 process-id=4033
user=mario user=mario

View File

@@ -683,3 +683,74 @@ java.lang.NullPointerException: Cannot invoke "org.eclipse.e4.ui.model.applicati
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563) at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415) at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387) at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-24 22:52:54.244
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
!SESSION 2026-03-25 07:26:05.291 -----------------------------------------------
eclipse.buildId=4.39.0.20260305-0817
java.version=21.0.6
java.vendor=Eclipse Adoptium
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
Framework arguments: -product org.eclipse.epp.package.java.product
Command-line arguments: -os linux -ws gtk -arch x86_64 -clean -product org.eclipse.epp.package.java.product
!ENTRY ch.qos.logback.classic 1 0 2026-03-25 07:26:06.750
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY ch.qos.logback.classic 1 0 2026-03-25 07:26:10.860
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
!ENTRY org.eclipse.ui 2 0 2026-03-25 07:26:11.009
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-25 07:26:11.009
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui 2 0 2026-03-25 07:26:11.149
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-25 07:26:11.149
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.jface 2 0 2026-03-25 11:50:50.380
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-25 11:50:50.380
!MESSAGE A conflict occurred for CTRL+R:
Binding(CTRL+R,
ParameterizedCommand(Command(org.eclipse.debug.ui.commands.RunToLine,Run to Line,
Resume and break when execution reaches the current line,
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
WorkbenchHandlerServiceHandler("org.eclipse.debug.ui.commands.RunToLine"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.debug.ui.debugging,,,system)
Binding(CTRL+R,
ParameterizedCommand(Command(org.springframework.ide.eclipse.boot.restart.commands.restart,Trigger Restart,
Restart Spring Boot Application,
Category(org.eclipse.debug.ui.category.run,Run/Debug,Run/Debug command category,true),
WorkbenchHandlerServiceHandler("org.springframework.ide.eclipse.boot.restart.commands.restart"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.debug.ui.console,,,system)
!ENTRY org.eclipse.jface 2 0 2026-03-25 16:00:33.268
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-25 16:00:33.268
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
Binding(CTRL+SHIFT+T,
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
Open a type in a Java editor,
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)
Binding(CTRL+SHIFT+T,
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
,
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-25 16:28:18.002
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS

View File

@@ -1,24 +1,7 @@
[ { [ {
"version" : "9.6.0-20260323005019+0000", "version" : "9.5.0-20260325015243+0000",
"buildTime" : "20260323005019+0000", "buildTime" : "20260325015243+0000",
"commitId" : "d20e3feb4da17c82d2df8774ea838cdd230628ce", "commitId" : "627839c6a3532eeab60306fd7b577d6f1c866ece",
"current" : false,
"snapshot" : true,
"nightly" : true,
"releaseNightly" : false,
"activeRc" : false,
"rcFor" : "",
"milestoneFor" : "",
"broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260323005019+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260323005019+0000-bin.zip.sha256",
"checksum" : "22865868bc4c8aa1f3cffb398397d3d6058246129490e168ecbbaaed24cd9fa1",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260323005019+0000-wrapper.jar.sha256",
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, {
"version" : "9.5.0-20260322013634+0000",
"buildTime" : "20260322013634+0000",
"commitId" : "01db0eb99f616dd415a084ffcce4cb2c185d5a2a",
"current" : false, "current" : false,
"snapshot" : true, "snapshot" : true,
"nightly" : false, "nightly" : false,
@@ -27,10 +10,27 @@
"rcFor" : "", "rcFor" : "",
"milestoneFor" : "", "milestoneFor" : "",
"broken" : false, "broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-bin.zip", "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-bin.zip.sha256", "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-bin.zip.sha256",
"checksum" : "3e8a6689594399f81087ad962b1c489e0ae57201af0c6c00ea63d9d07e48506e", "checksum" : "f031a868b2d9707fe07c78ad0888d4be5d6bb87e3f8a118ffbf3b0b497c32922",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-wrapper.jar.sha256", "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260325015243+0000-wrapper.jar.sha256",
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, {
"version" : "9.6.0-20260325005438+0000",
"buildTime" : "20260325005438+0000",
"commitId" : "4a2f60ed3e5db8c8eadc899d49da7c6abf7140ee",
"current" : false,
"snapshot" : true,
"nightly" : true,
"releaseNightly" : false,
"activeRc" : false,
"rcFor" : "",
"milestoneFor" : "",
"broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-bin.zip.sha256",
"checksum" : "1419ec3a9f2e924772188c709cd681d073c7c821dbd82beb9a6e8f6b78f7723b",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260325005438+0000-wrapper.jar.sha256",
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a" "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, { }, {
"version" : "9.4.1", "version" : "9.4.1",

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,14 @@
<typeInfoHistroy> <typeInfoHistroy>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1772437686926"/> <typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben{DefaultFiller.java[DefaultFiller" modifiers="1" timestamp="1772437686926"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{FillerController.java[FillerController" modifiers="1" timestamp="1772385528555"/> <typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{FillerController.java[FillerController" modifiers="1" timestamp="1772385528555"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.timelock{TimeLockService.java[TimeLockService" modifiers="1" timestamp="1774441060175"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.ttlock{TTLockService.java[TTLockService" modifiers="1" timestamp="1774375173709"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.user{UserRepository.java[UserRepository" modifiers="513" timestamp="1774016609131"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.ttlock{TTLockUserConfigEntity.java[TTLockUserConfigEntity" modifiers="1" timestamp="1774425822887"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.keyholder{KeyholderNotificationEntity.java[KeyholderNotificationEntity" modifiers="1" timestamp="1774386563354"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.ttlock{TTLockCallback.java[TTLockCallback" modifiers="1" timestamp="1774387007874"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.cardlock{CardLockController.java[CardLockController" modifiers="1" timestamp="1774422277518"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.cardlock{CardLockEntity.java[CardLockEntity" modifiers="1" timestamp="1774171624571"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.timelock{TimeLockController.java[TimeLockController" modifiers="1" timestamp="1774388746804"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.common{BaseLockEntity.java[BaseLockEntity" modifiers="1" timestamp="1774369125472"/>
</typeInfoHistroy> </typeInfoHistroy>

View File

@@ -1,2 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<qualifiedTypeNameHistroy/> <qualifiedTypeNameHistroy>
<fullyQualifiedTypeName name="jakarta.websocket.server.ServerEndpoint"/>
<fullyQualifiedTypeName name="org.springframework.stereotype.Service"/>
<fullyQualifiedTypeName name="org.springframework.http.HttpHeaders"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.RestController"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.RequestMapping"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTAuthService"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockService"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockTest"/>
<fullyQualifiedTypeName name="org.springframework.http.HttpStatusCode"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.CodeCreator"/>
<fullyQualifiedTypeName name="java.util.Random"/>
<fullyQualifiedTypeName name="java.util.Collections"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockService.TTLockDetailResponse"/>
<fullyQualifiedTypeName name="lombok.Data"/>
<fullyQualifiedTypeName name="org.springframework.http.ResponseEntity"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.PathVariable"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.GetMapping"/>
<fullyQualifiedTypeName name="java.lang.String"/>
<fullyQualifiedTypeName name="org.springframework.http.MediaType"/>
<fullyQualifiedTypeName name="java.util.Map"/>
<fullyQualifiedTypeName name="java.util.List"/>
<fullyQualifiedTypeName name="com.fasterxml.jackson.core.type.TypeReference"/>
<fullyQualifiedTypeName name="java.lang.Exception"/>
<fullyQualifiedTypeName name="java.time.LocalDateTime"/>
<fullyQualifiedTypeName name="java.time.Duration"/>
<fullyQualifiedTypeName name="java.util.Optional"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockUserConfigRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.ttlock.TTLockCallback"/>
</qualifiedTypeNameHistroy>

View File

@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<section name="Workbench"> <section name="Workbench">
<item key="filters_last_used" value="filter_imports;"/>
<section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart"> <section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart">
<item key="group_libraries" value="true"/> <item key="group_libraries" value="true"/>
<item key="layout" value="1"/> <item key="layout" value="1"/>
<item key="rootMode" value="1"/> <item key="rootMode" value="1"/>
<item key="linkWithEditor" value="false"/> <item key="linkWithEditor" value="true"/>
<item key="memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#x0A;&lt;packageExplorer group_libraries=&quot;1&quot; layout=&quot;1&quot; linkWithEditor=&quot;0&quot; rootMode=&quot;1&quot; workingSetName=&quot;Aggregate for window 1774277926242&quot;&gt;&#x0A;&lt;customFilters userDefinedPatternsEnabled=&quot;false&quot;&gt;&#x0A;&lt;xmlDefinedFilters&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.StaticsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.buildship.ui.packageexplorer.filter.gradle.buildfolder&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.mylyn.java.ui.MembersFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.m2e.MavenModuleFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.buildship.ui.packageexplorer.filter.gradle.subProject&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.DeprecatedMembersFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.FieldsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;/xmlDefinedFilters&gt;&#x0A;&lt;/customFilters&gt;&#x0A;&lt;/packageExplorer&gt;"/> <item key="memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#x0A;&lt;packageExplorer group_libraries=&quot;1&quot; layout=&quot;1&quot; linkWithEditor=&quot;1&quot; rootMode=&quot;1&quot; workingSetName=&quot;Aggregate for window 1774277926242&quot;&gt;&#x0A;&lt;customFilters userDefinedPatternsEnabled=&quot;false&quot;&gt;&#x0A;&lt;xmlDefinedFilters&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.StaticsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.buildship.ui.packageexplorer.filter.gradle.buildfolder&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.mylyn.java.ui.MembersFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.m2e.MavenModuleFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.buildship.ui.packageexplorer.filter.gradle.subProject&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.DeprecatedMembersFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.FieldsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0A;&lt;/xmlDefinedFilters&gt;&#x0A;&lt;/customFilters&gt;&#x0A;&lt;/packageExplorer&gt;"/>
</section> </section>
<section name="JavaElementSearchActions"> <section name="JavaElementSearchActions">
</section> </section>
@@ -25,4 +26,54 @@
</section> </section>
<section name="quick_assist_proposal_size"> <section name="quick_assist_proposal_size">
</section> </section>
<section name="NewClassCreationWizard.dialogBounds">
<item key="DIALOG_X_ORIGIN" value="981"/>
<item key="DIALOG_Y_ORIGIN" value="225"/>
<item key="DIALOG_WIDTH" value="599"/>
<item key="DIALOG_HEIGHT" value="689"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu|10.0|0|GTK|1|"/>
</section>
<section name="NewPackageCreationWizard.dialogBounds">
<item key="DIALOG_X_ORIGIN" value="1011"/>
<item key="DIALOG_Y_ORIGIN" value="408"/>
<item key="DIALOG_WIDTH" value="539"/>
<item key="DIALOG_HEIGHT" value="500"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu|10.0|0|GTK|1|"/>
</section>
<section name="NewPackageWizardPage">
<item key="create_package_info_java" value="false"/>
</section>
<section name="OptionalMessageDialog.hide.">
<item key="org.eclipse.jdt.ui.typecomment.deprecated" value="true"/>
</section>
<section name="NewClassWizardPage">
<item key="create_constructor" value="false"/>
<item key="create_unimplemented" value="true"/>
</section>
<section name="org.eclipse.jdt.internal.ui.text.QuickOutline">
<item key="GoIntoTopLevelTypeAction.isChecked" value="false"/>
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_WIDTH" value="400"/>
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_HEIGHT" value="340"/>
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_USE_PERSISTED_SIZE" value="true"/>
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_USE_PERSISTED_LOCATION" value="false"/>
</section>
<section name="RenameInformationPopup">
</section>
<section name="org.eclipse.ltk.ui.refactoring.settings">
<item key="updateSimilarElements" value="false"/>
<item key="updateSimilarElementsMatchStrategy" value="1"/>
<item key="updateTextualMatches" value="false"/>
<item key="updateQualifiedNames" value="false"/>
<item key="patterns" value="*"/>
</section>
<section name="org.eclipse.jdt.internal.ui.typehierarchy.QuickHierarchy">
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_WIDTH" value="400"/>
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_HEIGHT" value="340"/>
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_USE_PERSISTED_SIZE" value="true"/>
<item key="org.eclipse.jdt.internal.ui.typehierarchy.HierarchyInformationControlDIALOG_USE_PERSISTED_LOCATION" value="false"/>
</section>
<section name="RefactoringWizard.preview">
<item key="width" value="600"/>
<item key="height" value="400"/>
</section>
</section> </section>

View File

@@ -7,3 +7,4 @@
2026-03-23 21:09:44,347 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-23 21:09:44,347 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-24 06:41:47,661 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-24 06:41:47,661 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-24 11:26:24,107 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-24 11:26:24,107 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-25 07:26:14,133 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.

View File

@@ -1,3 +1,3 @@
#Tue Mar 24 11:25:59 CET 2026 #Wed Mar 25 07:26:10 CET 2026
org.eclipse.core.runtime=2 org.eclipse.core.runtime=2
org.eclipse.platform=4.39.0.v20260226-0420 org.eclipse.platform=4.39.0.v20260226-0420

BIN
bilder/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -31,6 +31,10 @@ import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository; import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository; import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository; import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.feedback.FeedbackEntity;
import de.oaa.xxx.feedback.FeedbackRepository;
import de.oaa.xxx.feedback.FeedbackStatus;
import de.oaa.xxx.support.SupportUserService;
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigEntity; import de.oaa.xxx.games.chastity.ttlock.TTLockConfigEntity;
import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository; import de.oaa.xxx.games.chastity.ttlock.TTLockConfigRepository;
import de.oaa.xxx.meldung.MeldungEntity; import de.oaa.xxx.meldung.MeldungEntity;
@@ -50,6 +54,8 @@ public class AdminController {
private final AdminRepository adminRepository; private final AdminRepository adminRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final MeldungRepository meldungRepository; private final MeldungRepository meldungRepository;
private final FeedbackRepository feedbackRepository;
private final SupportUserService supportUserService;
private final AufgabenGruppeRepository aufgabenGruppeRepository; private final AufgabenGruppeRepository aufgabenGruppeRepository;
private final AufgabeRepository aufgabeRepository; private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository; private final StrafeRepository strafeRepository;
@@ -62,6 +68,8 @@ public class AdminController {
public AdminController(AdminRepository adminRepository, UserRepository userRepository, public AdminController(AdminRepository adminRepository, UserRepository userRepository,
MeldungRepository meldungRepository, MeldungRepository meldungRepository,
FeedbackRepository feedbackRepository,
SupportUserService supportUserService,
AufgabenGruppeRepository aufgabenGruppeRepository, AufgabenGruppeRepository aufgabenGruppeRepository,
AufgabeRepository aufgabeRepository, AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository, StrafeRepository strafeRepository,
@@ -74,6 +82,8 @@ public class AdminController {
this.adminRepository = adminRepository; this.adminRepository = adminRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.meldungRepository = meldungRepository; this.meldungRepository = meldungRepository;
this.feedbackRepository = feedbackRepository;
this.supportUserService = supportUserService;
this.aufgabenGruppeRepository = aufgabenGruppeRepository; this.aufgabenGruppeRepository = aufgabenGruppeRepository;
this.aufgabeRepository = aufgabeRepository; this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository; this.strafeRepository = strafeRepository;
@@ -109,6 +119,12 @@ public class AdminController {
record SubscriptionStatusDto(UUID userId, String userName, String subscriptionType, record SubscriptionStatusDto(UUID userId, String userName, String subscriptionType,
LocalDate subscribedAt, LocalDate validUntil) {} LocalDate subscribedAt, LocalDate validUntil) {}
record FeedbackDto(UUID feedbackId, String name, String seite, String grund,
String text, LocalDateTime eingegangen, FeedbackStatus status,
String inArbeitVonName) {}
record FeedbackAntwortRequest(String text) {}
// ── Hilfsmethoden ──────────────────────────────────────────────────────── // ── Hilfsmethoden ────────────────────────────────────────────────────────
private AdminEntity requireAdmin(Principal principal) { private AdminEntity requireAdmin(Principal principal) {
@@ -413,6 +429,74 @@ public class AdminController {
)); ));
} }
// ── Feedback ─────────────────────────────────────────────────────────────
private FeedbackDto toFeedbackDto(FeedbackEntity e) {
String inArbeitName = null;
if (e.getInArbeitVon() != null) {
inArbeitName = userRepository.findById(e.getInArbeitVon())
.map(UserEntity::getName).orElse("?");
}
return new FeedbackDto(e.getFeedbackId(), e.getName(), e.getSeite(), e.getGrund(),
e.getText(), e.getEingegangen(), e.getStatus(), inArbeitName);
}
@GetMapping("/feedback")
public ResponseEntity<java.util.Map<String, List<FeedbackDto>>> getFeedback(Principal principal) {
requireAdmin(principal);
List<FeedbackDto> ungelesen = feedbackRepository
.findByStatusOrderByEingegangenDesc(FeedbackStatus.UNGELESEN)
.stream().map(this::toFeedbackDto).toList();
List<FeedbackDto> inArbeit = feedbackRepository
.findByStatusOrderByEingegangenDesc(FeedbackStatus.IN_ARBEIT)
.stream().map(this::toFeedbackDto).toList();
List<FeedbackDto> beantwortet = feedbackRepository
.findByStatusOrderByEingegangenDesc(FeedbackStatus.BEANTWORTET)
.stream().map(this::toFeedbackDto).toList();
return ResponseEntity.ok(java.util.Map.of(
"ungelesen", ungelesen,
"inArbeit", inArbeit,
"beantwortet", beantwortet));
}
@PutMapping("/feedback/{id}/annehmen")
public ResponseEntity<Void> feedbackAnnehmen(@PathVariable("id") UUID id, Principal principal) {
AdminEntity admin = requireAdmin(principal);
FeedbackEntity f = feedbackRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
if (f.getStatus() == FeedbackStatus.IN_ARBEIT) {
// Bereits von jemand anderem in Arbeit Konflikt
return ResponseEntity.status(409).build();
}
f.setStatus(FeedbackStatus.IN_ARBEIT);
f.setInArbeitVon(admin.getUserId());
feedbackRepository.save(f);
return ResponseEntity.noContent().build();
}
@PostMapping("/feedback/{id}/antworten")
public ResponseEntity<Void> feedbackAntworten(@PathVariable("id") UUID id,
@RequestBody FeedbackAntwortRequest body,
Principal principal) {
requireAdmin(principal);
if (body.text() == null || body.text().isBlank()) {
return ResponseEntity.badRequest().build();
}
FeedbackEntity f = feedbackRepository.findById(id)
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND));
f.setStatus(FeedbackStatus.BEANTWORTET);
feedbackRepository.save(f);
// DM an den Nutzer senden, falls er eingeloggt war
if (f.getUserId() != null) {
String dm = "Ursprüngliche Nachricht\n" + f.getText() + "\n\nAntwort\n" + body.text();
supportUserService.sendDm(f.getUserId(), dm);
}
return ResponseEntity.noContent().build();
}
// ── TTLock-Konfiguration (nur SUPERADMIN) ───────────────────────────────── // ── TTLock-Konfiguration (nur SUPERADMIN) ─────────────────────────────────
@GetMapping("/ttlock") @GetMapping("/ttlock")

View File

@@ -76,6 +76,7 @@ public class SecurityConfig {
.requestMatchers("/notifications/**").authenticated() .requestMatchers("/notifications/**").authenticated()
.requestMatchers("/events/**").authenticated() .requestMatchers("/events/**").authenticated()
.requestMatchers("/*.html").permitAll() .requestMatchers("/*.html").permitAll()
.requestMatchers("/help/*.html").permitAll()
.requestMatchers("/css/**").permitAll() .requestMatchers("/css/**").permitAll()
.requestMatchers("/js/**").permitAll() .requestMatchers("/js/**").permitAll()
.requestMatchers("/images/**").permitAll() .requestMatchers("/images/**").permitAll()
@@ -100,6 +101,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.POST, "/password-reset/confirm").permitAll() .requestMatchers(HttpMethod.POST, "/password-reset/confirm").permitAll()
.requestMatchers(HttpMethod.GET, "/email-change/**").permitAll() .requestMatchers(HttpMethod.GET, "/email-change/**").permitAll()
.requestMatchers(HttpMethod.GET, "/keyholder/invitation/**").permitAll() .requestMatchers(HttpMethod.GET, "/keyholder/invitation/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/feedback").permitAll()
.requestMatchers(HttpMethod.POST, "/filler").permitAll() .requestMatchers(HttpMethod.POST, "/filler").permitAll()
.requestMatchers(HttpMethod.POST, "/api/ttlock/callback").permitAll() .requestMatchers(HttpMethod.POST, "/api/ttlock/callback").permitAll()
.requestMatchers(HttpMethod.GET, "/api/ttlock/callback").permitAll() .requestMatchers(HttpMethod.GET, "/api/ttlock/callback").permitAll()

View File

@@ -0,0 +1,89 @@
package de.oaa.xxx.feedback;
import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.support.SupportUserService;
import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.UUID;
@RestController
@RequestMapping("/api/feedback")
public class FeedbackController {
private final MailService mailService;
private final FeedbackRepository feedbackRepository;
private final UserRepository userRepository;
private final SupportUserService supportUserService;
public FeedbackController(MailService mailService,
FeedbackRepository feedbackRepository,
UserRepository userRepository,
SupportUserService supportUserService) {
this.mailService = mailService;
this.feedbackRepository = feedbackRepository;
this.userRepository = userRepository;
this.supportUserService = supportUserService;
}
record FeedbackRequest(String name, String seite, String grund, String text) {}
@PostMapping
public ResponseEntity<Void> send(@RequestBody FeedbackRequest req, Principal principal) {
if (req.text() == null || req.text().isBlank() || req.text().length() < 10 || req.text().length() > 1000) {
return ResponseEntity.badRequest().build();
}
// Eingeloggten User ermitteln (optional)
UUID userId = null;
if (principal != null) {
userId = userRepository.findByEmail(principal.getName())
.map(u -> u.getUserId()).orElse(null);
}
FeedbackEntity entity = new FeedbackEntity();
entity.setUserId(userId);
entity.setName(req.name());
entity.setSeite(req.seite());
entity.setGrund(req.grund());
entity.setText(req.text());
entity.setEingegangen(LocalDateTime.now());
entity.setStatus(FeedbackStatus.UNGELESEN);
feedbackRepository.save(entity);
// Bestätigungs-DM an eingeloggten Nutzer
if (userId != null) {
supportUserService.sendDm(userId,
"Vielen Dank für dein Feedback! ✉️\n\n" +
"Wir haben deine Nachricht erhalten und werden uns so schnell wie möglich darum kümmern.\n\n" +
"Bitte antworte nicht auf diese Nachricht du kannst uns jederzeit über " +
"Kontakt & Feedback erneut erreichen.");
}
try {
Email email = new Email();
email.setEmailAdresse("kontakt@xxx-sphere.de");
email.setTitel("[xXx Sphere] " + esc(req.grund()));
email.setText(
"<b>Von:</b> " + esc(req.name()) + "<br>" +
"<b>Seite:</b> " + esc(req.seite()) + "<br>" +
"<b>Grund:</b> " + esc(req.grund()) + "<br><br>" +
"<b>Nachricht:</b><br>" + esc(req.text()).replace("\n", "<br>")
);
mailService.send(email);
} catch (Exception e) {
// Mail-Server nicht erreichbar Eintrag ist bereits gespeichert
}
return ResponseEntity.ok().build();
}
private String esc(String s) {
if (s == null) return "";
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
}

View File

@@ -0,0 +1,38 @@
package de.oaa.xxx.feedback;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "feedback")
@Getter
@Setter
public class FeedbackEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID feedbackId;
/** Eingeloggter Nutzer null wenn Gast */
private UUID userId;
private String name;
private String seite;
private String grund;
@Column(columnDefinition = "TEXT")
private String text;
private LocalDateTime eingegangen;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private FeedbackStatus status = FeedbackStatus.UNGELESEN;
/** Admin-UserId der den Eintrag in Arbeit genommen hat */
private UUID inArbeitVon;
}

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.feedback;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface FeedbackRepository extends JpaRepository<FeedbackEntity, UUID> {
List<FeedbackEntity> findByStatusOrderByEingegangenDesc(FeedbackStatus status);
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.feedback;
public enum FeedbackStatus {
UNGELESEN,
IN_ARBEIT,
BEANTWORTET
}

View File

@@ -595,6 +595,7 @@ public class CardLockController {
result.put("testLock", l.isTestLock()); result.put("testLock", l.isTestLock());
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null); result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
result.put("controllType", l.getControllType() != null ? l.getControllType().name() : "UNLOCK_CODE");
if (l.isTestLock()) { if (l.isTestLock()) {
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""); result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
} }

View File

@@ -118,7 +118,6 @@ public class TimeLockController {
TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false); TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false);
lock.setStartTime(null); lock.setStartTime(null);
lock.setUnlockTime(null);
timeLockRepository.save(lock); timeLockRepository.save(lock);
String token = UUID.randomUUID().toString().replace("-", ""); String token = UUID.randomUUID().toString().replace("-", "");
@@ -328,6 +327,7 @@ public class TimeLockController {
result.put("spinEnabled", spinEnabled); result.put("spinEnabled", spinEnabled);
result.put("spinDue", spinDue); result.put("spinDue", spinDue);
result.put("nextSpinIn", nextSpinIn); result.put("nextSpinIn", nextSpinIn);
result.put("spinningWheelEntries", l.getSpinningWheelEntries() != null ? l.getSpinningWheelEntries() : List.of());
result.put("taskTimingEnabled", taskTimingEnabled); result.put("taskTimingEnabled", taskTimingEnabled);
result.put("nextTaskIn", nextTaskIn); result.put("nextTaskIn", nextTaskIn);

View File

@@ -21,10 +21,6 @@ import lombok.Setter;
@DiscriminatorValue("TIMELOCK") @DiscriminatorValue("TIMELOCK")
public class TimeLockEntity extends BaseLockEntity { public class TimeLockEntity extends BaseLockEntity {
@Column
private LocalDateTime startTime;
@Column
private LocalDateTime unlockTime;
@Column @Column
private boolean endTimeVisible; private boolean endTimeVisible;
@Column @Column

View File

@@ -317,6 +317,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
} }
public void check() { public void check() {
if (lock.getStartTime() == null) return;
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now();
if (!lock.getStartTime().toLocalDate().equals(today)) { if (!lock.getStartTime().toLocalDate().equals(today)) {
if (lock.getLastCheck() != null || today.isAfter(lock.getLastCheck())) { if (lock.getLastCheck() != null || today.isAfter(lock.getLastCheck())) {

View File

@@ -29,4 +29,8 @@ public class TTLockUserConfigEntity {
/** ID des aktuell gesetzten PINs auf dem Schloss wird zum Löschen beim nächsten lock() benötigt */ /** ID des aktuell gesetzten PINs auf dem Schloss wird zum Löschen beim nächsten lock() benötigt */
@Column @Column
private Integer currentKeyboardPwdId; private Integer currentKeyboardPwdId;
/** true, wenn der Anwender mindestens einmal erfolgreich die Verbindung getestet hat */
@Column(nullable = false, columnDefinition = "boolean default false")
private boolean testSuccessful;
} }

View File

@@ -10,6 +10,7 @@ import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.MessageEntity; import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.FriendshipRepository; import de.oaa.xxx.social.repository.FriendshipRepository;
import de.oaa.xxx.social.repository.MessageRepository; import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.support.SupportUserService;
import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -219,6 +220,11 @@ public class SocialController {
if (body.text() == null || body.text().isBlank()) return ResponseEntity.badRequest().build(); if (body.text() == null || body.text().isBlank()) return ResponseEntity.badRequest().build();
// Nachrichten an den Support-Account sind nicht erlaubt
if (SupportUserService.SUPPORT_USER_ID.equals(body.receiverId())) {
return ResponseEntity.status(403).build();
}
MessageEntity msg = new MessageEntity(); MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID()); msg.setMessageId(UUID.randomUUID());
msg.setSenderId(myId); msg.setSenderId(myId);

View File

@@ -97,6 +97,7 @@ public class SystemMessageService {
case GAME_STATE -> "XXX The Game Spielstatus-Änderung"; case GAME_STATE -> "XXX The Game Spielstatus-Änderung";
case EMERGENCY -> "XXX The Game ⚠️ Notfall"; case EMERGENCY -> "XXX The Game ⚠️ Notfall";
case FRIENDREQUEST -> "XXX The Game Neue Freundschaftsanfrage"; case FRIENDREQUEST -> "XXX The Game Neue Freundschaftsanfrage";
case SUPPORT -> "xXx Sphere Nachricht vom Support";
}; };
} }

View File

@@ -4,5 +4,6 @@ public enum MessageCause {
INVITATION, INVITATION,
GAME_STATE, GAME_STATE,
EMERGENCY, EMERGENCY,
FRIENDREQUEST FRIENDREQUEST,
SUPPORT
} }

View File

@@ -0,0 +1,112 @@
package de.oaa.xxx.support;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.UUID;
@Service
public class SupportUserService {
private static final Logger LOGGER = LoggerFactory.getLogger(SupportUserService.class);
/** Deterministischer UUID ändert sich nie. */
public static final UUID SUPPORT_USER_ID =
UUID.nameUUIDFromBytes("xxxsphere-support".getBytes(StandardCharsets.UTF_8));
private static final String SUPPORT_NAME = "xXx Support";
private static final String SUPPORT_EMAIL = "support@system.local";
private final UserRepository userRepository;
private final MessageRepository messageRepository;
public SupportUserService(UserRepository userRepository, MessageRepository messageRepository) {
this.userRepository = userRepository;
this.messageRepository = messageRepository;
}
/** Stellt sicher, dass der Support-Fake-User in der DB existiert. */
@PostConstruct
public void ensureExists() {
try {
String icon = loadIconBase64();
userRepository.findById(SUPPORT_USER_ID).ifPresentOrElse(u -> {
if (u.getProfilePicture() == null && icon != null) {
try {
u.setProfilePicture(icon);
userRepository.save(u);
} catch (Exception e) {
LOGGER.warn("Support-User-Avatar konnte nicht gespeichert werden: {}", e.getMessage());
}
}
}, () -> {
UserEntity u = new UserEntity();
u.setUserId(SUPPORT_USER_ID);
u.setName(SUPPORT_NAME);
u.setEmail(SUPPORT_EMAIL);
u.setPassword("__SYSTEM__"); // kein gültiges Login
try {
u.setProfilePicture(icon);
userRepository.save(u);
} catch (Exception e) {
LOGGER.warn("Support-User konnte nicht mit Avatar gespeichert werden, versuche ohne: {}", e.getMessage());
u.setProfilePicture(null);
userRepository.save(u);
}
});
} catch (Exception e) {
LOGGER.error("Support-User konnte nicht initialisiert werden: {}", e.getMessage());
}
}
private String loadIconBase64() {
try {
byte[] bytes = new ClassPathResource("static/img/icon.png").getInputStream().readAllBytes();
BufferedImage original = ImageIO.read(new ByteArrayInputStream(bytes));
BufferedImage thumb = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = thumb.createGraphics();
g.drawImage(original.getScaledInstance(64, 64, Image.SCALE_SMOOTH), 0, 0, null);
g.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(thumb, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (Exception e) {
LOGGER.warn("Support-Avatar konnte nicht geladen werden: {}", e.getMessage());
return null;
}
}
/**
* Sendet eine Direktnachricht vom Support-Account an den Nutzer.
* Die Nachricht erscheint als normale DM (systemMessage = false),
* damit sie in nachrichten.html sichtbar ist.
*/
public void sendDm(UUID receiverId, String text) {
if (receiverId == null) return;
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(SUPPORT_USER_ID);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(false);
msg.setMessageCause(MessageCause.SUPPORT);
messageRepository.save(msg);
}
}

View File

@@ -83,7 +83,7 @@ public class UserController {
record ProfilePictureRequest(String picture, String pictureHq) {} record ProfilePictureRequest(String picture, String pictureHq) {}
record NameChangeRequest(String name) {} record NameChangeRequest(String name) {}
record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {} record GeburtsdatumChangeRequest(LocalDate geburtsdatum) {}
record TtlockUserConfigDto(String username, boolean passwordSet, Integer lockId) {} record TtlockUserConfigDto(String username, boolean passwordSet, Integer lockId, boolean testSuccessful) {}
record TtlockUserConfigRequest(String username, String password, Integer lockId) {} record TtlockUserConfigRequest(String username, String password, Integer lockId) {}
record ProfileRequest(Integer groesse, Integer gewicht, record ProfileRequest(Integer groesse, Integer gewicht,
Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {} Geschlecht geschlecht, Neigung neigung, Beziehungsstatus beziehungsstatus, String beschreibung) {}
@@ -308,7 +308,8 @@ public class UserController {
return ResponseEntity.ok(new TtlockUserConfigDto( return ResponseEntity.ok(new TtlockUserConfigDto(
cfg.getUsername(), cfg.getUsername(),
cfg.getPasswordMd5() != null && !cfg.getPasswordMd5().isBlank(), cfg.getPasswordMd5() != null && !cfg.getPasswordMd5().isBlank(),
cfg.getLockId() cfg.getLockId(),
cfg.isTestSuccessful()
)); ));
} }
@@ -319,6 +320,12 @@ public class UserController {
UUID userId = userOpt.get().getUserId(); UUID userId = userOpt.get().getUserId();
TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userId) TTLockUserConfigEntity cfg = ttLockUserConfigRepository.findById(userId)
.orElseGet(() -> { TTLockUserConfigEntity n = new TTLockUserConfigEntity(); n.setUserId(userId); return n; }); .orElseGet(() -> { TTLockUserConfigEntity n = new TTLockUserConfigEntity(); n.setUserId(userId); return n; });
boolean credentialsChanged = !java.util.Objects.equals(cfg.getUsername(), body.username())
|| !java.util.Objects.equals(cfg.getLockId(), body.lockId())
|| (body.password() != null && !body.password().isBlank());
if (credentialsChanged) {
cfg.setTestSuccessful(false);
}
cfg.setUsername(body.username()); cfg.setUsername(body.username());
if (body.password() != null && !body.password().isBlank()) { if (body.password() != null && !body.password().isBlank()) {
cfg.setPasswordMd5(DigestUtils.md5DigestAsHex(body.password().getBytes(StandardCharsets.UTF_8))); cfg.setPasswordMd5(DigestUtils.md5DigestAsHex(body.password().getBytes(StandardCharsets.UTF_8)));
@@ -357,6 +364,9 @@ public class UserController {
return ResponseEntity.status(502).body(Map.of("error", "lock_detail_failed", "message", msg)); return ResponseEntity.status(502).body(Map.of("error", "lock_detail_failed", "message", msg));
} }
userCfg.setTestSuccessful(true);
ttLockUserConfigRepository.save(userCfg);
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
result.put("lockId", detail.getLockId()); result.put("lockId", detail.getLockId());
result.put("lockName", detail.getLockName()); result.put("lockName", detail.getLockName());

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Abonnements XXX The Game</title> <title>Abonnements xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXX The Game Aktivierung</title> <title>Aktivierung xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chastity Game XXX The Game</title> <title>Chastity Game xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -719,6 +719,15 @@
</div> </div>
</div> </div>
<!-- TTLock Lade-Dialog -->
<div class="warn-modal-backdrop" id="ttlLoadingModal">
<div class="warn-modal-box" style="text-align:center;padding:2rem 1.5rem;">
<div style="font-size:2rem;margin-bottom:0.75rem;"></div>
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
</div>
</div>
<!-- Warn-Modal (TestLock beenden) --> <!-- Warn-Modal (TestLock beenden) -->
<div class="warn-modal-backdrop" id="warnModal"> <div class="warn-modal-backdrop" id="warnModal">
<div class="warn-modal-box"> <div class="warn-modal-box">
@@ -762,7 +771,7 @@
<script src="/js/shared.js"></script> <script src="/js/shared.js"></script>
<script src="/js/card-defs.js"></script> <script src="/js/card-defs.js"></script>
<script src="/js/card-display.js"></script> <script src="/js/card-display.js"></script>
<script src="/js/icons.js"></script> <script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script> <script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script> <script src="/js/social-sidebar.js"></script>
<script> <script>
@@ -1421,7 +1430,21 @@
async function endHygieneOpening() { async function endHygieneOpening() {
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; } if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK';
if (isTtlock) {
document.getElementById('hygieneModal').classList.remove('open');
document.getElementById('ttlLoadingModal').classList.add('open');
}
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' }); const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
if (isTtlock) {
document.getElementById('ttlLoadingModal').classList.remove('open');
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
loadLock();
return;
}
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; } if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
const data = await res.json(); const data = await res.json();
@@ -1497,6 +1520,9 @@
async function lockLoeschen() { async function lockLoeschen() {
closeWarnModal(); closeWarnModal();
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
document.getElementById('ttlLoadingModal').classList.add('open');
}
try { try {
await fetch('/keyholder/cardlock/' + lockId, { method: 'DELETE' }); await fetch('/keyholder/cardlock/' + lockId, { method: 'DELETE' });
} catch (_) { /* ignorieren */ } } catch (_) { /* ignorieren */ }

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TimeLock XXX The Game</title> <title>TimeLock xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -166,6 +166,23 @@
.vote-up { color: #2ecc71; font-weight: 600; } .vote-up { color: #2ecc71; font-weight: 600; }
.vote-down { color: #e74c3c; font-weight: 600; } .vote-down { color: #e74c3c; font-weight: 600; }
/* ── Glücksrad-Animation ── */
.wheel-anim-backdrop {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.82); z-index: 600;
align-items: center; justify-content: center;
}
.wheel-anim-backdrop.open { display: flex; }
.wheel-canvas-wrap {
position: relative; display: flex; flex-direction: column; align-items: center;
}
.wheel-pointer-top {
font-size: 2.2rem; line-height: 1; margin-bottom: -12px; z-index: 1;
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.8));
color: #e74c3c;
}
#wheelCanvas { border-radius: 50%; display: block; }
/* ── Spin-Result-Modal ── */ /* ── Spin-Result-Modal ── */
.spin-modal-backdrop { .spin-modal-backdrop {
display: none; position: fixed; inset: 0; display: none; position: fixed; inset: 0;
@@ -295,7 +312,7 @@
<!-- Spin-Panel --> <!-- Spin-Panel -->
<div class="spin-panel" id="spinPanel" style="display:none;"> <div class="spin-panel" id="spinPanel" style="display:none;">
<div> <div>
<div class="spin-panel-title">Spinning Wheel</div> <div class="spin-panel-title">Glücksrad</div>
<div class="spin-countdown" id="spinCountdown"></div> <div class="spin-countdown" id="spinCountdown"></div>
</div> </div>
<button class="btn-spin" id="spinBtn" onclick="doSpin()">🎡 Drehen</button> <button class="btn-spin" id="spinBtn" onclick="doSpin()">🎡 Drehen</button>
@@ -346,6 +363,14 @@
</div> </div>
</div> </div>
<!-- Glücksrad-Animation-Modal -->
<div class="wheel-anim-backdrop" id="wheelAnimModal">
<div class="wheel-canvas-wrap">
<div class="wheel-pointer-top"></div>
<canvas id="wheelCanvas" width="290" height="290"></canvas>
</div>
</div>
<!-- Spin-Result-Modal --> <!-- Spin-Result-Modal -->
<div class="spin-modal-backdrop" id="spinModal"> <div class="spin-modal-backdrop" id="spinModal">
<div class="spin-modal-box"> <div class="spin-modal-box">
@@ -578,9 +603,169 @@
tickInterval = setInterval(tick, 1000); tickInterval = setInterval(tick, 1000);
} }
// ── Glücksrad-Animation ─────────────────────────────────────────────────────
let wheelEntries = [];
let wheelResult = null;
let wheelAnimFrame = null;
let wheelLastTime = null;
let wheelAngle = 0;
let wheelAnimState = 'idle';
let wheelSpinStartTime = 0;
let wheelDecelStartTime = 0;
let wheelDecelStartAngle = 0;
let wheelTargetAngle = 0;
const WHEEL_MAX_SPEED = 0.014; // rad/ms
const WHEEL_ACCEL_MS = 500;
const WHEEL_DECEL_MS = 2500;
const WHEEL_MIN_SPIN_MS = 1000;
const WHEEL_COLORS = [
'#c0392b','#2980b9','#27ae60','#d35400',
'#8e44ad','#16a085','#e67e22','#c0392b',
'#1565c0','#2e7d32'
];
const WHEEL_LABELS = {
ADD_TIME: '+ Zeit',
REMOVE_TIME: ' Zeit',
FREEZE_TIME: '❄ Einfrieren',
FREEZE: '🧊 Freeze',
UNFREEZE: '🌊 Auftauen',
TASK: '🎯 Aufgabe',
TEXT: '💬 Text',
};
function startWheelSpin() {
document.getElementById('wheelAnimModal').classList.add('open');
wheelAngle = Math.random() * 2 * Math.PI;
wheelResult = null;
wheelAnimState = 'accelerating';
wheelLastTime = null;
wheelSpinStartTime = performance.now();
if (wheelAnimFrame) cancelAnimationFrame(wheelAnimFrame);
wheelAnimFrame = requestAnimationFrame(wheelTick);
}
function wheelTick(now) {
if (!wheelLastTime) wheelLastTime = now;
const dt = Math.min(now - wheelLastTime, 50);
wheelLastTime = now;
const canvas = document.getElementById('wheelCanvas');
if (!canvas || !wheelEntries.length) return;
if (wheelAnimState === 'accelerating') {
const p = Math.min((now - wheelSpinStartTime) / WHEEL_ACCEL_MS, 1);
wheelAngle += WHEEL_MAX_SPEED * p * dt;
if (p >= 1) { wheelAnimState = 'spinning'; wheelSpinStartTime = now; }
} else if (wheelAnimState === 'spinning') {
wheelAngle += WHEEL_MAX_SPEED * dt;
if (wheelResult && (now - wheelSpinStartTime) >= WHEEL_MIN_SPIN_MS) {
initWheelDecel();
}
} else if (wheelAnimState === 'decelerating') {
const p = Math.min((now - wheelDecelStartTime) / WHEEL_DECEL_MS, 1);
const eased = 1 - Math.pow(1 - p, 4);
wheelAngle = wheelDecelStartAngle + eased * (wheelTargetAngle - wheelDecelStartAngle);
if (p >= 1) {
wheelAngle = wheelTargetAngle;
wheelAnimState = 'done';
drawWheelFrame(canvas, wheelAngle);
setTimeout(() => {
document.getElementById('wheelAnimModal').classList.remove('open');
showSpinResult(wheelResult);
loadLock();
}, 750);
return;
}
}
drawWheelFrame(canvas, wheelAngle);
wheelAnimFrame = requestAnimationFrame(wheelTick);
}
function initWheelDecel() {
const n = wheelEntries.length;
let idx = wheelEntries.findIndex(e =>
e.type === wheelResult.type &&
e.intVal === wheelResult.intVal &&
e.stringVal === wheelResult.stringVal
);
if (idx < 0) idx = wheelEntries.findIndex(e => e.type === wheelResult.type);
if (idx < 0) idx = 0;
const segCenter = ((idx + 0.5) / n) * 2 * Math.PI;
const rawTarget = -Math.PI / 2 - segCenter;
const targetMod = ((rawTarget % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI);
const currMod = ((wheelAngle % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI);
let extra = targetMod - currMod;
if (extra < 0) extra += 2 * Math.PI;
wheelDecelStartAngle = wheelAngle;
wheelTargetAngle = wheelAngle + extra + 2 * 2 * Math.PI;
wheelDecelStartTime = performance.now();
wheelAnimState = 'decelerating';
}
function drawWheelFrame(canvas, angle) {
const ctx = canvas.getContext('2d');
const w = canvas.width, h = canvas.height;
const cx = w/2, cy = h/2;
const r = Math.min(w, h)/2 - 6;
const n = wheelEntries.length;
if (!n) return;
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < n; i++) {
const a0 = angle + (i / n) * 2 * Math.PI;
const a1 = angle + ((i+1) / n) * 2 * Math.PI;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, a0, a1);
ctx.closePath();
ctx.fillStyle = WHEEL_COLORS[i % WHEEL_COLORS.length];
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 2;
ctx.stroke();
const aMid = angle + ((i+0.5) / n) * 2 * Math.PI;
const fontSize = Math.max(9, Math.min(13, Math.floor(170 / n)));
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(aMid);
ctx.textAlign = 'right';
ctx.fillStyle = '#fff';
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 3;
ctx.fillText(WHEEL_LABELS[wheelEntries[i].type] || wheelEntries[i].type, r - 10, 4);
ctx.restore();
}
// Outer ring
ctx.beginPath();
ctx.arc(cx, cy, r, 0, 2*Math.PI);
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
ctx.lineWidth = 5;
ctx.stroke();
// Center circle
ctx.beginPath();
ctx.arc(cx, cy, 17, 0, 2*Math.PI);
ctx.fillStyle = '#111122';
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.lineWidth = 2;
ctx.stroke();
}
// ── Spin-Panel ───────────────────────────────────────────────────────────── // ── Spin-Panel ─────────────────────────────────────────────────────────────
function renderSpinPanel(lock) { function renderSpinPanel(lock) {
wheelEntries = lock.spinningWheelEntries || [];
if (spinTickInterval) { clearInterval(spinTickInterval); spinTickInterval = null; } if (spinTickInterval) { clearInterval(spinTickInterval); spinTickInterval = null; }
const panel = document.getElementById('spinPanel'); const panel = document.getElementById('spinPanel');
@@ -622,18 +807,24 @@
async function doSpin() { async function doSpin() {
const btn = document.getElementById('spinBtn'); const btn = document.getElementById('spinBtn');
btn.disabled = true; btn.disabled = true;
startWheelSpin();
try { try {
const res = await fetch('/keyholder/timelock/' + lockId + '/spin', { method: 'POST' }); const res = await fetch('/keyholder/timelock/' + lockId + '/spin', { method: 'POST' });
if (!res.ok) { if (!res.ok) {
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
document.getElementById('wheelAnimModal').classList.remove('open');
if (wheelAnimFrame) { cancelAnimationFrame(wheelAnimFrame); wheelAnimFrame = null; }
wheelAnimState = 'idle';
alert(data.error || 'Spin nicht möglich.'); alert(data.error || 'Spin nicht möglich.');
btn.disabled = false; btn.disabled = false;
return; return;
} }
const result = await res.json(); wheelResult = await res.json();
showSpinResult(result); // Animation läuft weiter Ergebnis wird nach Abschluss angezeigt
loadLock();
} catch(e) { } catch(e) {
document.getElementById('wheelAnimModal').classList.remove('open');
if (wheelAnimFrame) { cancelAnimationFrame(wheelAnimFrame); wheelAnimFrame = null; }
wheelAnimState = 'idle';
alert('Fehler beim Drehen.'); alert('Fehler beim Drehen.');
btn.disabled = false; btn.disabled = false;
} }

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Administration XXX The Game</title> <title>Administration xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -435,6 +435,7 @@
<div class="tabs"> <div class="tabs">
<button class="tab-btn active" data-tab="meldungen">Meldungen</button> <button class="tab-btn active" data-tab="meldungen">Meldungen</button>
<button class="tab-btn" data-tab="feedback">Feedback</button>
<button class="tab-btn" data-tab="aufgabengruppen">Aufgabengruppen</button> <button class="tab-btn" data-tab="aufgabengruppen">Aufgabengruppen</button>
<button class="tab-btn" data-tab="toys">Toys</button> <button class="tab-btn" data-tab="toys">Toys</button>
<button class="tab-btn superadmin-only" data-tab="admins">Admins</button> <button class="tab-btn superadmin-only" data-tab="admins">Admins</button>
@@ -468,6 +469,47 @@
</div> </div>
</div> </div>
<!-- ── Feedback ── -->
<div class="tab-panel" id="panel-feedback">
<div class="section">
<div class="section-header">
<h2 class="section-title">📬 Ungelesen</h2>
<div class="section-actions">
<button class="btn-action" onclick="loadFeedback()">↺ Aktualisieren</button>
</div>
</div>
<div id="feedback-ungelesen-list"><span class="loading">Wird geladen…</span></div>
</div>
<div class="section">
<div class="section-header">
<h2 class="section-title" style="color:#f39c12">🔧 In Arbeit</h2>
</div>
<div id="feedback-inarbeit-list"><span class="loading">Wird geladen…</span></div>
</div>
<div class="section">
<div class="section-header">
<h2 class="section-title" style="color:var(--color-muted)">✅ Beantwortet</h2>
</div>
<div id="feedback-beantwortet-list"><span class="loading">Wird geladen…</span></div>
</div>
</div>
<!-- ── Feedback Antwort-Modal ── -->
<div class="modal-backdrop" id="feedbackAntwortModal">
<div class="modal">
<h2>✉️ Antwort senden</h2>
<input type="hidden" id="feedbackAntwortId">
<label>Antworttext</label>
<textarea id="feedbackAntwortText" rows="6" placeholder="Nachricht an den Nutzer…" style="width:100%;box-sizing:border-box;"></textarea>
<p style="font-size:0.8rem;color:var(--color-muted);margin-top:0.5rem;">Der Nutzer erhält diese Nachricht als Direktnachricht vom Support-Account.</p>
<div class="modal-actions">
<button class="btn-cancel" onclick="closeFeedbackAntwort()">Abbrechen</button>
<button class="btn-save" onclick="submitFeedbackAntwort()">Absenden</button>
</div>
<div class="modal-error" id="feedbackAntwortError"></div>
</div>
</div>
<!-- ── Aufgabengruppen ── --> <!-- ── Aufgabengruppen ── -->
<div class="tab-panel" id="panel-aufgabengruppen"> <div class="tab-panel" id="panel-aufgabengruppen">
<div class="section"> <div class="section">
@@ -644,6 +686,7 @@ async function init() {
document.querySelectorAll('.superadmin-only').forEach(el => el.classList.remove('superadmin-only')); document.querySelectorAll('.superadmin-only').forEach(el => el.classList.remove('superadmin-only'));
} }
loadMeldungen(); loadMeldungen();
loadFeedback();
loadAdminGruppen(); loadAdminGruppen();
loadAdminToys(); loadAdminToys();
if (admin.rolle === 'SUPERADMIN') { loadAdmins(); loadTtlockConfig(); loadAllSubscriptions(); } if (admin.rolle === 'SUPERADMIN') { loadAdmins(); loadTtlockConfig(); loadAllSubscriptions(); }
@@ -664,11 +707,114 @@ document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => {
btn.classList.add('active'); btn.classList.add('active');
document.getElementById('panel-' + btn.dataset.tab).classList.add('active'); document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
localStorage.setItem('tab_admin', btn.dataset.tab); localStorage.setItem('tab_admin', btn.dataset.tab);
if (btn.dataset.tab === 'feedback') loadFeedback();
}); });
}); });
// ── Meldungen ───────────────────────────────────────────────────────────── // ── Meldungen ─────────────────────────────────────────────────────────────
async function loadFeedback() {
const r = await fetch('/admin/feedback');
if (!r.ok) return;
const data = await r.json();
renderFeedbackList('feedback-ungelesen-list', data.ungelesen, 'ungelesen');
renderFeedbackList('feedback-inarbeit-list', data.inArbeit, 'inArbeit');
renderFeedbackList('feedback-beantwortet-list', data.beantwortet, 'beantwortet');
}
function renderFeedbackList(containerId, list, status) {
const el = document.getElementById(containerId);
const emptyTexts = { ungelesen: 'Keine ungelesenen Einträge.', inArbeit: 'Niemand arbeitet gerade an einem Eintrag.', beantwortet: 'Noch keine beantworteten Einträge.' };
if (!list || list.length === 0) {
el.innerHTML = '<span class="empty">' + (emptyTexts[status] || '') + '</span>';
return;
}
el.innerHTML = list.map(f => {
const actions = status === 'ungelesen'
? `<button class="btn-item-edit" onclick="feedbackAnnehmen('${f.feedbackId}',event)">🔧 In Arbeit nehmen</button>`
: status === 'inArbeit'
? `<button class="btn-item-edit" onclick="openFeedbackAntwort('${f.feedbackId}',event)">✉️ Antworten &amp; abschließen</button>`
: '';
const inArbeitBadge = f.inArbeitVonName
? `<span class="badge" style="background:rgba(243,156,18,0.15);color:#f39c12;">🔧 ${esc(f.inArbeitVonName)}</span>`
: '';
return `
<div class="item" id="fb-item-${f.feedbackId}">
<div class="item-row" onclick="toggleFbItem('${f.feedbackId}')">
<span class="item-text"><strong>${esc(f.name)}</strong> ${esc(f.grund)}</span>
<span class="item-badges">
${inArbeitBadge}
<span class="badge badge-neutral">${esc(f.seite)}</span>
<span class="badge badge-neutral">${formatDate(f.eingegangen)}</span>
</span>
</div>
<div class="item-detail">
<div class="item-detail-row">
<span class="item-detail-label">Von:</span><span class="item-detail-chip">${esc(f.name)}</span>
<span class="item-detail-label">Seite:</span><span class="item-detail-chip">${esc(f.seite)}</span>
<span class="item-detail-label">Grund:</span><span class="item-detail-chip">${esc(f.grund)}</span>
</div>
<div class="item-detail-text">${esc(f.text)}</div>
${actions ? `<div class="item-action-btns">${actions}</div>` : ''}
</div>
</div>`;
}).join('');
}
function toggleFbItem(id) {
document.getElementById('fb-item-' + id)?.classList.toggle('open');
}
async function feedbackAnnehmen(id, e) {
e?.stopPropagation();
const res = await fetch('/admin/feedback/' + id + '/annehmen', { method: 'PUT' });
if (res.status === 409) {
alert('Dieser Eintrag wird bereits von jemand anderem bearbeitet.');
}
loadFeedback();
}
function openFeedbackAntwort(id, e) {
e?.stopPropagation();
document.getElementById('feedbackAntwortId').value = id;
document.getElementById('feedbackAntwortText').value = '';
document.getElementById('feedbackAntwortError').style.display = 'none';
document.getElementById('feedbackAntwortModal').classList.add('open');
}
function closeFeedbackAntwort() {
document.getElementById('feedbackAntwortModal').classList.remove('open');
}
async function submitFeedbackAntwort() {
const id = document.getElementById('feedbackAntwortId').value;
const text = document.getElementById('feedbackAntwortText').value.trim();
const errEl = document.getElementById('feedbackAntwortError');
if (!text) { errEl.textContent = 'Bitte einen Text eingeben.'; errEl.style.display = 'block'; return; }
const res = await fetch('/admin/feedback/' + id + '/antworten', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (res.ok) {
closeFeedbackAntwort();
loadFeedback();
} else {
errEl.textContent = 'Fehler beim Senden.';
errEl.style.display = 'block';
}
}
function formatDate(dt) {
if (!dt) return '';
return new Date(dt).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' });
}
function esc(s) {
if (!s) return '';
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
async function loadMeldungen() { async function loadMeldungen() {
const filter = document.getElementById('meldungFilter').value; const filter = document.getElementById('meldungFilter').value;
const r = await fetch('/admin/meldungen' + (filter ? '?status=' + filter : '')); const r = await fetch('/admin/meldungen' + (filter ? '?status=' + filter : ''));

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aufgaben XXX The Game</title> <title>Aufgaben xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Einladung XXX The Game</title> <title>BDSM Game Einladung xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title> <title>BDSM Game xXx Sphere</title>
</head> </head>
<body> <body>
<script>window.location.replace('/neubdsm.html');</script> <script>window.location.replace('/neubdsm.html');</script>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Im Spiel XXX The Game</title> <title>BDSM Game Im Spiel xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title> <title>BDSM Game xXx Sphere</title>
</head> </head>
<body> <body>
<script>window.location.replace('/neubdsm.html');</script> <script>window.location.replace('/neubdsm.html');</script>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title> <title>BDSM Game xXx Sphere</title>
</head> </head>
<body> <body>
<script>window.location.replace('/neubdsm.html');</script> <script>window.location.replace('/neubdsm.html');</script>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title> <title>BDSM Game xXx Sphere</title>
</head> </head>
<body> <body>
<script>window.location.replace('/neubdsm.html');</script> <script>window.location.replace('/neubdsm.html');</script>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=/neubdsm.html"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<title>BDSM Game</title> <title>BDSM Game xXx Sphere</title>
</head> </head>
<body> <body>
<script>window.location.replace('/neubdsm.html');</script> <script>window.location.replace('/neubdsm.html');</script>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Benachrichtigungen XXX The Game</title> <title>Benachrichtigungen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profil XXX The Game</title> <title>Profil xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -539,7 +539,7 @@
const isFriend = profile.friendStatus === 'FRIEND'; const isFriend = profile.friendStatus === 'FRIEND';
document.title = profile.name + ' XXX The Game'; document.title = profile.name + ' xXx Sphere';
renderHeader(profile); renderHeader(profile);
// ── Galerie ── // ── Galerie ──

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Community Votes XXX The Game</title> <title>Community Votes xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -188,20 +188,32 @@ body.app {
.content { padding: 2rem 1.5rem; flex: 1; } .content { padding: 2rem 1.5rem; flex: 1; }
/* ── Sidebar ── */ /* ── Sidebar ── */
.sidebar { .sidebar-wrapper {
width: 240px; width: 240px;
flex-shrink: 0; flex-shrink: 0;
display: flex;
flex-direction: column;
align-self: stretch;
gap: 0.75rem;
z-index: 10;
transition: transform 0.25s ease;
}
.sidebar {
flex: 1;
min-height: 0;
background: var(--color-card); background: var(--color-card);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-self: stretch; overflow: hidden;
position: static; }
.sidebar-scroll-area {
flex: 1;
overflow-y: auto; overflow-y: auto;
z-index: 10;
transition: transform 0.25s ease;
} }
.sidebar-logo-area { .sidebar-logo-area {
@@ -257,7 +269,8 @@ body.app {
.sidebar ul { list-style: none; padding: 0.5rem 0; } .sidebar ul { list-style: none; padding: 0.5rem 0; }
.sidebar ul li a { .sidebar ul li a,
.sidebar-footer ul li a {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
@@ -270,13 +283,16 @@ body.app {
} }
.sidebar ul li a:hover, .sidebar ul li a:hover,
.sidebar ul li a.active { .sidebar ul li a.active,
.sidebar-footer ul li a:hover,
.sidebar-footer ul li a.active {
background: var(--color-secondary); background: var(--color-secondary);
color: var(--color-primary); color: var(--color-primary);
border-left-color: var(--color-primary); border-left-color: var(--color-primary);
} }
.sidebar ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; } .sidebar ul li a .icon,
.sidebar-footer ul li a .icon { font-size: 1rem; width: 1.2rem; text-align: center; flex-shrink: 0; }
.sidebar-profile-img { .sidebar-profile-img {
width: 1.4rem; width: 1.4rem;
@@ -344,22 +360,38 @@ body.app {
overflow: visible; overflow: visible;
} }
.sidebar { .sidebar-wrapper {
position: fixed; position: fixed;
top: 0; right: 0; top: 0; right: 0;
width: 240px; width: 240px;
height: 100vh; height: 100vh;
max-height: 100vh; gap: 0;
border-radius: 0; background: var(--color-bg);
border: none;
border-left: 1px solid var(--color-secondary); border-left: 1px solid var(--color-secondary);
box-shadow: none;
transform: translateX(100%); transform: translateX(100%);
align-self: auto; align-self: auto;
z-index: 100; z-index: 100;
padding: 0;
overflow-y: auto;
}
.sidebar-wrapper.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); }
.sidebar {
flex: none;
border-radius: 0;
border: none;
box-shadow: none;
border-bottom: 1px solid var(--color-secondary);
}
.sidebar-footer {
border-radius: 0;
box-shadow: none;
border: none;
border-top: 1px solid var(--color-secondary);
} }
.sidebar.open { transform: translateX(0); box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5); }
.sidebar-logo-area { display: none; } .sidebar-logo-area { display: none; }
.sidebar-desktop-profile { display: none; } .sidebar-desktop-profile { display: none; }
@@ -471,6 +503,19 @@ body.app {
} }
/* ── Sidebar groups ── */ /* ── Sidebar groups ── */
.sidebar-footer {
flex-shrink: 0;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
padding: 0.5rem 0;
}
.sidebar-footer ul {
list-style: none;
padding: 0;
}
.sidebar-group-toggle { .sidebar-group-toggle {
cursor: pointer; cursor: pointer;
justify-content: space-between; justify-content: space-between;
@@ -523,10 +568,28 @@ body.app {
padding: 0.55rem 1rem; padding: 0.55rem 1rem;
} }
/* Linker Platzhalter gleiche Breite wie Sidebar */ /* Linker Bereich Banner, gleiche Breite wie Sidebar */
.topbar-left { .topbar-left {
width: 240px; width: 240px;
flex-shrink: 0; flex-shrink: 0;
align-self: stretch;
margin: -0.55rem 0 -0.55rem -1rem;
overflow: hidden;
border-radius: 11px 0 0 11px;
display: flex;
align-items: center;
padding: 5px 0 0 5px;
justify-content: center;
}
.topbar-left a {
display: flex;
align-items: center;
height: 100%;
}
.topbar-banner {
height: 3.5rem;
width: auto;
display: block;
} }
/* ── Suche ── */ /* ── Suche ── */

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einladungen XXX The Game</title> <title>Einladungen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einstellungen XXX The Game</title> <title>Einstellungen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -588,7 +588,11 @@
<p style="font-size:0.85rem;color:var(--color-muted);margin:0.75rem 0 1.25rem 0;"> <p style="font-size:0.85rem;color:var(--color-muted);margin:0.75rem 0 1.25rem 0;">
Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern. Verknüpfe deinen TTLock-Account, um deine physische Schlüsselbox direkt über das Spiel zu steuern.
</p> </p>
<div style="margin-top:1rem;font-size:0.82rem;">
<a href="/help/ttlock.html" style="color:var(--color-muted);">❓ Hilfe zur TTLock-Integration</a>
</div>
<div class="spiel-field"> <div class="spiel-field">
<div class="settings-row-label">Benutzername (E-Mail)</div> <div class="settings-row-label">Benutzername (E-Mail)</div>
<input type="text" id="ttl-username" placeholder="E-Mail-Adresse bei TTLock" <input type="text" id="ttl-username" placeholder="E-Mail-Adresse bei TTLock"
@@ -598,7 +602,8 @@
<div class="spiel-field"> <div class="spiel-field">
<div class="settings-row-label">Passwort <span id="ttl-pw-hint" style="font-size:0.78rem;color:var(--color-muted);font-weight:400;"></span></div> <div class="settings-row-label">Passwort <span id="ttl-pw-hint" style="font-size:0.78rem;color:var(--color-muted);font-weight:400;"></span></div>
<input type="password" id="ttl-password" placeholder="Leer lassen = unverändert" <input type="password" id="ttl-password" placeholder="Leer lassen = unverändert"
style="margin-top:0.3rem;" autocomplete="new-password"> style="margin-top:0.3rem;" autocomplete="new-password"
onfocus="ttlPwFocus()" onblur="ttlPwBlur()">
</div> </div>
<div class="spiel-field"> <div class="spiel-field">
@@ -609,6 +614,12 @@
</div> </div>
<div id="ttl-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-bottom:0.5rem;"></div> <div id="ttl-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-bottom:0.5rem;"></div>
<!-- Verbindungsstatus -->
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
<span style="font-size:0.82rem;color:var(--color-muted);">Verbindungsstatus:</span>
<span id="ttl-test-status" style="font-size:0.82rem;font-weight:600;"></span>
</div>
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;align-items:center;"> <div style="display:flex;gap:0.6rem;flex-wrap:wrap;align-items:center;">
<button onclick="saveTtlockUserConfig()" style="width:auto;padding:0.5rem 1.25rem;margin:0;">Speichern</button> <button onclick="saveTtlockUserConfig()" style="width:auto;padding:0.5rem 1.25rem;margin:0;">Speichern</button>
<button onclick="testTtlockConnection()" id="ttl-test-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔌 Verbindung testen</button> <button onclick="testTtlockConnection()" id="ttl-test-btn" style="width:auto;padding:0.5rem 1.25rem;margin:0;background:none;border:1px solid var(--color-secondary);color:var(--color-muted);">🔌 Verbindung testen</button>
@@ -620,12 +631,23 @@
<!-- Öffnen-Ergebnis --> <!-- Öffnen-Ergebnis -->
<div id="ttl-open-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-top:0.5rem;"></div> <div id="ttl-open-error" style="font-size:0.82rem;color:var(--color-primary);min-height:1.1em;margin-top:0.5rem;"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- TTLock Lade-Dialog -->
<div class="modal-backdrop" id="ttlLoadingModal">
<div class="modal" style="max-width:320px;text-align:center;padding:2rem 1.5rem;">
<div style="font-size:2rem;margin-bottom:0.75rem;"></div>
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
</div>
</div>
<!-- TTLock Öffnen Modal --> <!-- TTLock Öffnen Modal -->
<div class="modal-backdrop" id="ttlOpenModal"> <div class="modal-backdrop" id="ttlOpenModal">
<div class="modal" style="max-width:380px;text-align:center;"> <div class="modal" style="max-width:380px;text-align:center;">
@@ -1087,29 +1109,74 @@
// ── TTLock ────────────────────────────────────────────────────────────── // ── TTLock ──────────────────────────────────────────────────────────────
let _ttlPasswordSet = false;
const TTL_PW_DOTS = '••••••';
async function loadTtlockUserConfig() { async function loadTtlockUserConfig() {
const r = await fetch('/user/me/ttlock'); const r = await fetch('/user/me/ttlock');
if (!r.ok) return; if (!r.ok) return;
const cfg = await r.json(); const cfg = await r.json();
document.getElementById('ttl-username').value = cfg.username || ''; document.getElementById('ttl-username').value = cfg.username || '';
document.getElementById('ttl-lockid').value = cfg.lockId != null ? cfg.lockId : ''; document.getElementById('ttl-lockid').value = cfg.lockId != null ? cfg.lockId : '';
document.getElementById('ttl-password').value = ''; _ttlPasswordSet = !!cfg.passwordSet;
document.getElementById('ttl-pw-hint').textContent = const pwField = document.getElementById('ttl-password');
cfg.passwordSet ? '(gesetzt leer lassen zum Beibehalten)' : '(noch nicht gesetzt)'; if (_ttlPasswordSet) {
pwField.value = TTL_PW_DOTS;
document.getElementById('ttl-pw-hint').textContent = '';
} else {
pwField.value = '';
document.getElementById('ttl-pw-hint').textContent = '(noch nicht gesetzt)';
}
renderTtlockTestStatus(cfg.testSuccessful);
}
function renderTtlockTestStatus(ok) {
const el = document.getElementById('ttl-test-status');
if (ok) {
el.textContent = '✅ Verbindung erfolgreich getestet';
el.style.color = '#27ae60';
} else {
el.textContent = '⚠️ Noch nicht getestet';
el.style.color = 'var(--color-muted)';
}
}
function ttlPwFocus() {
const pwField = document.getElementById('ttl-password');
if (pwField.value === TTL_PW_DOTS) {
pwField.value = '';
document.getElementById('ttl-pw-hint').textContent = '(leer lassen = unverändert)';
}
}
function ttlPwBlur() {
const pwField = document.getElementById('ttl-password');
if (pwField.value === '' && _ttlPasswordSet) {
pwField.value = TTL_PW_DOTS;
document.getElementById('ttl-pw-hint').textContent = '';
}
}
function showTtlockLoading() {
document.getElementById('ttlLoadingModal').classList.add('visible');
}
function hideTtlockLoading() {
document.getElementById('ttlLoadingModal').classList.remove('visible');
} }
async function testTtlockConnection() { async function testTtlockConnection() {
const btn = document.getElementById('ttl-test-btn'); const btn = document.getElementById('ttl-test-btn');
const result = document.getElementById('ttl-test-result'); const result = document.getElementById('ttl-test-result');
btn.disabled = true; btn.disabled = true;
btn.textContent = '⏳ Teste…';
result.style.display = 'none'; result.style.display = 'none';
showTtlockLoading();
try { try {
const r = await fetch('/user/me/ttlock/test'); const r = await fetch('/user/me/ttlock/test');
const data = await r.json(); const data = await r.json();
if (r.ok) { if (r.ok) {
renderTtlockTestStatus(true);
const battery = data.electricQuantity != null ? `${data.electricQuantity}%` : ''; const battery = data.electricQuantity != null ? `${data.electricQuantity}%` : '';
const state = data.state || ''; const state = data.state || '';
result.style.background = 'rgba(39,174,96,0.08)'; result.style.background = 'rgba(39,174,96,0.08)';
@@ -1138,6 +1205,8 @@
result.style.background = 'rgba(231,76,60,0.08)'; result.style.background = 'rgba(231,76,60,0.08)';
result.style.borderColor = '#e74c3c'; result.style.borderColor = '#e74c3c';
result.innerHTML = `<div style="color:#e74c3c;">❌ Netzwerkfehler.</div>`; result.innerHTML = `<div style="color:#e74c3c;">❌ Netzwerkfehler.</div>`;
} finally {
hideTtlockLoading();
} }
result.style.display = ''; result.style.display = '';
@@ -1153,8 +1222,8 @@
const btn = document.getElementById('ttl-open-btn'); const btn = document.getElementById('ttl-open-btn');
const errEl = document.getElementById('ttl-open-error'); const errEl = document.getElementById('ttl-open-error');
btn.disabled = true; btn.disabled = true;
btn.textContent = '⏳ …';
errEl.textContent = ''; errEl.textContent = '';
showTtlockLoading();
try { try {
const r = await fetch('/user/me/ttlock/open', { method: 'POST' }); const r = await fetch('/user/me/ttlock/open', { method: 'POST' });
@@ -1173,6 +1242,7 @@
const pwdId = data.keyboardPwdId; const pwdId = data.keyboardPwdId;
document.getElementById('ttl-open-pin').textContent = data.pin; document.getElementById('ttl-open-pin').textContent = data.pin;
hideTtlockLoading();
const modal = document.getElementById('ttlOpenModal'); const modal = document.getElementById('ttlOpenModal');
modal.classList.add('visible'); modal.classList.add('visible');
@@ -1182,11 +1252,14 @@
okBtn.removeEventListener('click', onOk); okBtn.removeEventListener('click', onOk);
okBtn.disabled = true; okBtn.disabled = true;
modal.classList.remove('visible'); modal.classList.remove('visible');
showTtlockLoading();
await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' }); await fetch(`/user/me/ttlock/open/${pwdId}`, { method: 'DELETE' });
hideTtlockLoading();
okBtn.disabled = false; okBtn.disabled = false;
}; };
okBtn.addEventListener('click', onOk); okBtn.addEventListener('click', onOk);
} catch { } catch {
hideTtlockLoading();
errEl.textContent = 'Netzwerkfehler.'; errEl.textContent = 'Netzwerkfehler.';
} finally { } finally {
btn.disabled = false; btn.disabled = false;
@@ -1198,9 +1271,10 @@
const errEl = document.getElementById('ttl-error'); const errEl = document.getElementById('ttl-error');
errEl.textContent = ''; errEl.textContent = '';
const lockIdVal = document.getElementById('ttl-lockid').value.trim(); const lockIdVal = document.getElementById('ttl-lockid').value.trim();
const pwVal = document.getElementById('ttl-password').value;
const body = { const body = {
username: document.getElementById('ttl-username').value.trim(), username: document.getElementById('ttl-username').value.trim(),
password: document.getElementById('ttl-password').value, password: pwVal === TTL_PW_DOTS ? '' : pwVal,
lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null lockId: lockIdVal !== '' ? parseInt(lockIdVal) : null
}; };
const r = await fetch('/user/me/ttlock', { const r = await fetch('/user/me/ttlock', {

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entdecken XXX The Game</title> <title>Entdecken xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed XXX The Game</title> <title>Feed xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xXx Games Passwort vergessen</title> <title>Passwort vergessen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Freunde XXX The Game</title> <title>Freunde xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gruppe XXX The Game</title> <title>Gruppe xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -389,7 +389,7 @@
gruppeData = await res.json(); gruppeData = await res.json();
myRole = gruppeData.myRole; myRole = gruppeData.myRole;
document.title = gruppeData.name + ' XXX The Game'; document.title = gruppeData.name + ' xXx Sphere';
document.getElementById('gruppeName').textContent = gruppeData.name; document.getElementById('gruppeName').textContent = gruppeData.name;
document.getElementById('gruppeMeta').textContent = document.getElementById('gruppeMeta').textContent =
gruppeData.memberCount + ' Mitglied' + (gruppeData.memberCount !== 1 ? 'er' : '') + gruppeData.memberCount + ' Mitglied' + (gruppeData.memberCount !== 1 ? 'er' : '') +

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gruppen XXX The Game</title> <title>Gruppen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impressum xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header {
margin-bottom: 2rem;
}
.hilfe-header h1 {
font-size: 1.6rem;
margin: 0 0 0.4rem 0;
}
.hilfe-header p {
color: var(--color-muted);
font-size: 0.92rem;
margin: 0;
line-height: 1.6;
}
.impressum-block {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 0.75rem;
}
.impressum-block h2 {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-muted);
margin: 0 0 0.75rem 0;
}
.impressum-block p,
.impressum-block address {
font-size: 0.9rem;
color: var(--color-muted);
line-height: 1.8;
margin: 0;
font-style: normal;
}
.impressum-block a {
color: var(--color-muted);
text-decoration: none;
transition: color 0.15s;
}
.impressum-block a:hover {
color: var(--color-text);
}
.impressum-block + .impressum-block {
margin-top: 0;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div class="hilfe-header">
<h1>📄 Impressum</h1>
<p>Angaben gemäß § 5 TMG</p>
</div>
<div class="impressum-block">
<h2>Verantwortlich</h2>
<address>
Vorname Nachname<br>
Musterstraße 1<br>
12345 Musterstadt<br>
Deutschland
</address>
</div>
<div class="impressum-block">
<h2>Kontakt</h2>
<p>
E-Mail: <a href="mailto:kontakt@xxx-sphere.de">kontakt@xxx-sphere.de</a>
</p>
</div>
<div class="impressum-block">
<h2>Hinweis</h2>
<p>
xXx Sphere ist ein privat betriebenes Projekt ohne kommerzielle Absicht.
Die Plattform richtet sich ausschließlich an volljährige Personen.
</p>
</div>
<div class="impressum-block">
<h2>Haftungsausschluss</h2>
<p>
Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links.
Für den Inhalt verlinkter Seiten sind ausschließlich deren Betreiber verantwortlich.
</p>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
</body>
</html>

View File

@@ -0,0 +1,265 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kontakt & Feedback xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header {
margin-bottom: 2rem;
}
.hilfe-header h1 {
font-size: 1.6rem;
margin: 0 0 0.4rem 0;
}
.hilfe-header p {
color: var(--color-muted);
font-size: 0.92rem;
margin: 0;
line-height: 1.6;
}
/* ── E-Mail-Hinweis ── */
.mail-hint {
background: rgba(var(--color-primary-rgb, 120,80,200), 0.08);
border-left: 3px solid var(--color-primary);
border-radius: 0 8px 8px 0;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.mail-hint strong { color: var(--color-text); }
.mail-hint a { color: var(--color-text); }
/* ── Formular-Card ── */
.feedback-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 1.1rem;
}
.form-group:last-of-type {
margin-bottom: 1.5rem;
}
.form-group label {
font-size: 0.82rem;
font-weight: 600;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-group input,
.form-group select,
.form-group textarea {
background: var(--color-secondary);
border: 1px solid transparent;
border-radius: 8px;
padding: 0.65rem 0.9rem;
color: var(--color-text);
font-size: 0.92rem;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: var(--color-primary);
}
.form-group input[readonly],
.form-group input:disabled {
opacity: 0.55;
cursor: default;
}
.form-group textarea {
resize: vertical;
min-height: 130px;
}
.form-group select option {
background: var(--color-card);
}
.char-counter {
font-size: 0.78rem;
color: var(--color-muted);
text-align: right;
}
.char-counter.warn { color: #e74c3c; }
.btn-send {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.7rem 1.5rem;
background: var(--color-primary);
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
}
.btn-send:hover { opacity: 0.85; }
.btn-send:disabled { opacity: 0.45; cursor: default; }
.feedback-success {
display: none;
background: rgba(39,174,96,0.1);
border-left: 3px solid #27ae60;
border-radius: 0 8px 8px 0;
padding: 0.75rem 1rem;
font-size: 0.9rem;
color: var(--color-muted);
margin-top: 1rem;
}
.feedback-success strong { color: #27ae60; }
.feedback-error {
display: none;
background: rgba(231,76,60,0.08);
border-left: 3px solid #e74c3c;
border-radius: 0 8px 8px 0;
padding: 0.75rem 1rem;
font-size: 0.9rem;
color: var(--color-muted);
margin-top: 1rem;
}
.feedback-error strong { color: #e74c3c; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div class="hilfe-header">
<h1>✉️ Kontakt &amp; Feedback</h1>
<p>Hast du Fragen, Ideen oder einen Fehler gefunden? Schreib uns!</p>
</div>
<div class="mail-hint">
<strong>Alternativ per E-Mail:</strong> Du kannst uns auch direkt schreiben
<a href="mailto:kontakt@xxx-sphere.de">kontakt@xxx-sphere.de</a>
</div>
<div class="feedback-card">
<div class="form-group">
<label for="fb-name">Name</label>
<input type="text" id="fb-name" readonly placeholder="Wird geladen…">
</div>
<div class="form-group">
<label for="fb-seite">Seite</label>
<input type="text" id="fb-seite" readonly>
</div>
<div class="form-group">
<label for="fb-grund">Kontaktgrund</label>
<select id="fb-grund">
<option value="Fehlermeldung">🐛 Fehlermeldung</option>
<option value="Feedback">💬 Feedback</option>
<option value="Idee & Verbesserungsvorschlag">💡 Idee &amp; Verbesserungsvorschlag</option>
<option value="Nachfrage">❓ Nachfrage</option>
</select>
</div>
<div class="form-group">
<label for="fb-text">Nachricht</label>
<textarea id="fb-text" maxlength="1000" placeholder="Beschreibe dein Anliegen…"></textarea>
<span class="char-counter" id="fb-counter">0 / 1000</span>
</div>
<button class="btn-send" id="fb-send" onclick="sendFeedback()">✉️ Absenden</button>
<div class="feedback-success" id="fb-success">
<strong>Vielen Dank!</strong> Deine Nachricht wurde erfolgreich übermittelt.
</div>
<div class="feedback-error" id="fb-error">
<strong>Fehler:</strong> Die Nachricht konnte nicht gesendet werden. Bitte versuche es später erneut.
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
// Name vorausfüllen
fetch('/login/me')
.then(r => r.ok ? r.json() : null)
.then(user => {
const field = document.getElementById('fb-name');
if (user && user.name) {
field.value = user.name;
} else {
field.value = 'Gast';
}
})
.catch(() => { document.getElementById('fb-name').value = 'Gast'; });
// Seite vorausfüllen
(function () {
const field = document.getElementById('fb-seite');
try {
const ref = document.referrer;
if (ref) {
const url = new URL(ref);
field.value = url.pathname;
} else {
field.value = 'Direkt aufgerufen';
}
} catch (_) {
field.value = 'Unbekannt';
}
})();
// Zeichenzähler
document.getElementById('fb-text').addEventListener('input', function () {
const len = this.value.length;
const counter = document.getElementById('fb-counter');
counter.textContent = len + ' / 1000';
counter.className = 'char-counter' + (len > 900 || len < 10 ? ' warn' : '');
});
async function sendFeedback() {
const btn = document.getElementById('fb-send');
const text = document.getElementById('fb-text').value.trim();
if (text.length < 10) { document.getElementById('fb-text').focus(); return; }
btn.disabled = true;
document.getElementById('fb-success').style.display = 'none';
document.getElementById('fb-error').style.display = 'none';
try {
const res = await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: document.getElementById('fb-name').value,
seite: document.getElementById('fb-seite').value,
grund: document.getElementById('fb-grund').value,
text: text
})
});
if (res.ok) {
document.getElementById('fb-success').style.display = 'block';
document.getElementById('fb-text').value = '';
document.getElementById('fb-counter').textContent = '0 / 1000';
} else {
document.getElementById('fb-error').style.display = 'block';
btn.disabled = false;
}
} catch (_) {
document.getElementById('fb-error').style.display = 'block';
btn.disabled = false;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hilfe-Übersicht xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.hilfe-header {
margin-bottom: 2rem;
}
.hilfe-header h1 {
font-size: 1.6rem;
margin: 0 0 0.4rem 0;
}
.hilfe-header p {
color: var(--color-muted);
font-size: 0.92rem;
margin: 0;
line-height: 1.6;
}
/* ── Kategorien-Grid ── */
.hilfe-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.hilfe-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.hilfe-card-title {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1rem;
font-weight: 700;
border-bottom: 1px solid var(--color-secondary);
padding-bottom: 0.6rem;
}
.hilfe-card-desc {
font-size: 0.85rem;
color: var(--color-muted);
line-height: 1.6;
}
.hilfe-card-links {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-top: 0.25rem;
}
.hilfe-card-links a {
font-size: 0.85rem;
color: var(--color-muted);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.4rem;
transition: color 0.15s;
}
.hilfe-card-links a:hover {
color: var(--color-text);
}
.hilfe-card-links a::before {
content: '';
font-size: 1rem;
color: var(--color-primary);
flex-shrink: 0;
}
/* ── Abschnitt-Überschrift ── */
.hilfe-section-label {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-muted);
margin: 1.75rem 0 0.75rem;
}
.hilfe-section-label:first-child {
margin-top: 0;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<div class="hilfe-header">
<h1>❓ Hilfe-Übersicht</h1>
<p>Hier findest du Anleitungen und Erklärungen zu allen Bereichen von xXx Sphere.</p>
</div>
<!-- ── Einstellungen & Konto ── -->
<div class="hilfe-section-label">Einstellungen &amp; Konto</div>
<div class="hilfe-grid">
<div class="hilfe-card">
<div class="hilfe-card-title">⚙️ Allgemeine Einstellungen</div>
<div class="hilfe-card-desc">Profil, Benachrichtigungen, Datenschutz und weitere Kontoeinstellungen.</div>
<div class="hilfe-card-links">
<a href="#">Profil bearbeiten</a>
<a href="#">Benachrichtigungen konfigurieren</a>
<a href="#">Passwort ändern</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">🔒 TTLock-Integration</div>
<div class="hilfe-card-desc">Verbinde deine physische Schlüsselbox mit xXx Sphere für automatische Code-Verwaltung.</div>
<div class="hilfe-card-links">
<a href="/help/ttlock.html#sec-intro">Was ist TTLock?</a>
<a href="/help/ttlock.html#sec-table">Voraussetzungen</a>
<a href="/help/ttlock.html#sec-howto">TTLock einrichten</a>
<a href="/help/ttlock.html#sec-faq1">Warum nur für Abonnenten?</a>
<a href="/help/ttlock.html#sec-faq2">Notfall-Öffnung</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">💳 Abonnements</div>
<div class="hilfe-card-desc">Informationen zu Premium-Funktionen und wie du dein Abonnement verwaltest.</div>
<div class="hilfe-card-links">
<a href="#">Premium-Funktionen im Überblick</a>
<a href="#">Abonnement kündigen</a>
</div>
</div>
</div>
<!-- ── Spiele ── -->
<div class="hilfe-section-label">Spiele</div>
<div class="hilfe-grid">
<div class="hilfe-card">
<div class="hilfe-card-title">🔒 Chastity Game</div>
<div class="hilfe-card-desc">Alles rund um Schlösser, Keyholder, Karten und Aufgaben im Chastity Game.</div>
<div class="hilfe-card-links">
<a href="#">Neues Lock starten</a>
<a href="#">Die Rolle als Keyholder</a>
<a href="#">Karten und Aufgaben</a>
<a href="#">TimeLock erklärt</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">⛓️ BDSM Game</div>
<div class="hilfe-card-desc">Sessions erstellen, Spieler einladen und Aufgaben verwalten.</div>
<div class="hilfe-card-links">
<a href="#">Session starten</a>
<a href="#">Spieler einladen</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">⚪ Vanilla Game</div>
<div class="hilfe-card-desc">Leichtere Spiele ohne strenge Regeln für den entspannten Einstieg.</div>
<div class="hilfe-card-links">
<a href="#">Vanilla-Session starten</a>
</div>
</div>
</div>
<!-- ── Community ── -->
<div class="hilfe-section-label">Community</div>
<div class="hilfe-grid">
<div class="hilfe-card">
<div class="hilfe-card-title">👥 Gruppen</div>
<div class="hilfe-card-desc">Gruppen erstellen, beitreten und verwalten.</div>
<div class="hilfe-card-links">
<a href="#">Gruppe erstellen</a>
<a href="#">Mitglieder verwalten</a>
<a href="#">Beiträge und Abstimmungen</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">📰 Feed &amp; Profil</div>
<div class="hilfe-card-desc">Beiträge teilen, Profile entdecken und die Community kennenlernen.</div>
<div class="hilfe-card-links">
<a href="#">Feed nutzen</a>
<a href="#">Profil gestalten</a>
<a href="#">Personen suchen und folgen</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">🏆 Community Votes</div>
<div class="hilfe-card-desc">Verifikationen bewerten und an Community-Abstimmungen teilnehmen.</div>
<div class="hilfe-card-links">
<a href="#">Wie funktionieren Votes?</a>
</div>
</div>
</div>
<!-- ── Sonstiges ── -->
<div class="hilfe-section-label">Sonstiges</div>
<div class="hilfe-grid">
<div class="hilfe-card">
<div class="hilfe-card-title">🔐 Sicherheit &amp; Datenschutz</div>
<div class="hilfe-card-desc">Wie deine Daten gespeichert werden und welche Sicherheitsmaßnahmen wir treffen.</div>
<div class="hilfe-card-links">
<a href="#">Datenspeicherung</a>
<a href="#">Passwort-Hashing</a>
</div>
</div>
<div class="hilfe-card">
<div class="hilfe-card-title">🐛 Fehler melden</div>
<div class="hilfe-card-desc">Hast du einen Fehler gefunden oder einen Verbesserungsvorschlag?</div>
<div class="hilfe-card-links">
<a href="#">Feedback senden</a>
</div>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
</body>
</html>

View File

@@ -0,0 +1,345 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SEITENTITEL xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Hilfe-Seite Basis ── */
.hilfe-header {
margin-bottom: 2rem;
}
.hilfe-header h1 {
font-size: 1.6rem;
margin: 0 0 0.4rem 0;
}
.hilfe-header p {
color: var(--color-muted);
font-size: 0.92rem;
margin: 0;
line-height: 1.6;
}
/* ── Abschnitte (Accordion) ── */
.hilfe-section {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
margin-bottom: 0.75rem;
overflow: hidden;
}
.hilfe-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 1rem 1.25rem;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.hilfe-section-header:hover {
background: rgba(255,255,255,0.03);
}
.hilfe-section-title {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1rem;
font-weight: 600;
}
.hilfe-section-arrow {
font-size: 0.75rem;
color: var(--color-muted);
transition: transform 0.2s;
}
.hilfe-section.open .hilfe-section-arrow {
transform: rotate(90deg);
}
.hilfe-section-body {
display: none;
padding: 0 1.25rem 1.25rem;
border-top: 1px solid var(--color-secondary);
}
.hilfe-section.open .hilfe-section-body {
display: block;
}
/* ── Fließtext in Abschnitten ── */
.hilfe-section-body p {
font-size: 0.9rem;
color: var(--color-muted);
line-height: 1.7;
margin: 0.9rem 0 0;
}
.hilfe-section-body p:first-child {
margin-top: 1rem;
}
/* ── Schritt-für-Schritt Liste ── */
.hilfe-steps {
list-style: none;
padding: 0;
margin: 1rem 0 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.hilfe-steps li {
display: flex;
align-items: flex-start;
gap: 0.85rem;
font-size: 0.9rem;
color: var(--color-muted);
line-height: 1.6;
}
.hilfe-steps li .step-num {
flex-shrink: 0;
width: 1.6rem;
height: 1.6rem;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
font-size: 0.75rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-top: 0.1rem;
}
/* ── Hinweis-Box ── */
.hilfe-hint {
background: rgba(var(--color-primary-rgb, 120,80,200), 0.08);
border-left: 3px solid var(--color-primary);
border-radius: 0 8px 8px 0;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.6;
margin-top: 1rem;
}
.hilfe-hint strong {
color: var(--color-text);
}
/* ── Warn-Box ── */
.hilfe-warn {
background: rgba(231,76,60,0.08);
border-left: 3px solid #e74c3c;
border-radius: 0 8px 8px 0;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.6;
margin-top: 1rem;
}
.hilfe-warn strong {
color: #e74c3c;
}
/* ── Info-Box (neutral) ── */
.hilfe-info {
background: var(--color-secondary);
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.6;
margin-top: 1rem;
}
/* ── Einfache Tabelle ── */
.hilfe-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
margin-top: 1rem;
}
.hilfe-table th {
text-align: left;
color: var(--color-text);
font-weight: 600;
padding: 0.4rem 0.75rem 0.4rem 0;
border-bottom: 1px solid var(--color-secondary);
}
.hilfe-table td {
color: var(--color-muted);
padding: 0.5rem 0.75rem 0.5rem 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
vertical-align: top;
line-height: 1.5;
}
.hilfe-table tr:last-child td {
border-bottom: none;
}
/* ── Trennlinie ── */
.hilfe-divider {
border: none;
border-top: 1px solid var(--color-secondary);
margin: 1.25rem 0 0;
}
/* ── Inline-Badge ── */
.hilfe-badge {
display: inline-block;
background: var(--color-secondary);
border-radius: 4px;
padding: 0.1rem 0.45rem;
font-size: 0.78rem;
font-weight: 600;
color: var(--color-muted);
vertical-align: middle;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<!-- ── Kopfzeile ─────────────────────────────────────── -->
<div class="hilfe-header">
<h1>🔒 SEITENTITEL</h1>
<p>Kurze Beschreibung, worum es auf dieser Hilfeseite geht.</p>
</div>
<!-- ── Abschnitt: Einfacher Fließtext ───────────────── -->
<div class="hilfe-section open" id="sec-intro">
<div class="hilfe-section-header" onclick="toggleSection('sec-intro')">
<span class="hilfe-section-title">📖 Was ist das?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Hier steht ein einleitender Text. Du kannst mehrere Absätze verwenden,
um das Thema zu erklären.
</p>
<p>
Zweiter Absatz mit weiteren Informationen. Links gehen so:
<a href="/neulock.html">neues Lock starten</a>.
</p>
<!-- Hinweis-Box (lila) -->
<div class="hilfe-hint">
<strong>Hinweis:</strong> Hier steht ein wichtiger, aber freundlicher Hinweis.
</div>
</div>
</div>
<!-- ── Abschnitt: Schritt-für-Schritt ───────────────── -->
<div class="hilfe-section" id="sec-howto">
<div class="hilfe-section-header" onclick="toggleSection('sec-howto')">
<span class="hilfe-section-title">🚀 So funktioniert es</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>Führe diese Schritte der Reihe nach aus:</p>
<ol class="hilfe-steps">
<li>
<span class="step-num">1</span>
<span>Erster Schritt was der Nutzer hier tun muss.</span>
</li>
<li>
<span class="step-num">2</span>
<span>Zweiter Schritt weitere Aktion mit Erklärung.</span>
</li>
<li>
<span class="step-num">3</span>
<span>Dritter Schritt Abschluss oder Ergebnis.</span>
</li>
</ol>
<!-- Warn-Box (rot) -->
<div class="hilfe-warn">
<strong>Achtung:</strong> Hier steht eine Warnung, z. B. dass eine Aktion nicht rückgängig gemacht werden kann.
</div>
</div>
</div>
<!-- ── Abschnitt: Tabelle ────────────────────────────── -->
<div class="hilfe-section" id="sec-table">
<div class="hilfe-section-header" onclick="toggleSection('sec-table')">
<span class="hilfe-section-title">📋 Übersicht</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<table class="hilfe-table">
<thead>
<tr>
<th>Funktion</th>
<th>Beschreibung</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="hilfe-badge">Beispiel A</span></td>
<td>Erklärung zu Funktion A.</td>
</tr>
<tr>
<td><span class="hilfe-badge">Beispiel B</span></td>
<td>Erklärung zu Funktion B.</td>
</tr>
<tr>
<td><span class="hilfe-badge">Beispiel C</span></td>
<td>Erklärung zu Funktion C.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ── Abschnitt: FAQ ────────────────────────────────── -->
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Häufige Frage 1?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Antwort auf die erste häufige Frage. Kann auch mehrere Absätze haben.
</p>
<div class="hilfe-info">
Neutrale Info-Box für ergänzende Details ohne Wertung.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Häufige Frage 2?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Antwort auf die zweite häufige Frage.
</p>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

View File

@@ -0,0 +1,360 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hilfe TTLock xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Hilfe-Seite Basis ── */
.hilfe-header {
margin-bottom: 2rem;
}
.hilfe-header h1 {
font-size: 1.6rem;
margin: 0 0 0.4rem 0;
}
.hilfe-header p {
color: var(--color-muted);
font-size: 0.92rem;
margin: 0;
line-height: 1.6;
}
/* ── Abschnitte (Accordion) ── */
.hilfe-section {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
margin-bottom: 0.75rem;
overflow: hidden;
}
.hilfe-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 1rem 1.25rem;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.hilfe-section-header:hover {
background: rgba(255,255,255,0.03);
}
.hilfe-section-title {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1rem;
font-weight: 600;
}
.hilfe-section-arrow {
font-size: 0.75rem;
color: var(--color-muted);
transition: transform 0.2s;
}
.hilfe-section.open .hilfe-section-arrow {
transform: rotate(90deg);
}
.hilfe-section-body {
display: none;
padding: 0 1.25rem 1.25rem;
border-top: 1px solid var(--color-secondary);
}
.hilfe-section.open .hilfe-section-body {
display: block;
}
/* ── Fließtext in Abschnitten ── */
.hilfe-section-body p {
font-size: 0.9rem;
color: var(--color-muted);
line-height: 1.7;
margin: 0.9rem 0 0;
}
.hilfe-section-body p:first-child {
margin-top: 1rem;
}
/* ── Schritt-für-Schritt Liste ── */
.hilfe-steps {
list-style: none;
padding: 0;
margin: 1rem 0 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.hilfe-steps li {
list-style: none;
display: flex;
align-items: flex-start;
gap: 0.85rem;
font-size: 0.9rem;
color: var(--color-muted);
line-height: 1.6;
}
.hilfe-steps li::before {
display: none;
}
.hilfe-steps li .step-num {
flex-shrink: 0;
width: 1.6rem;
height: 1.6rem;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
font-size: 0.75rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-top: 0.1rem;
}
.hilfe-steps li strong,
.hilfe-steps li em {
color: inherit;
}
/* ── Hinweis-Box ── */
.hilfe-hint {
background: rgba(var(--color-primary-rgb, 120,80,200), 0.08);
border-left: 3px solid var(--color-primary);
border-radius: 0 8px 8px 0;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.6;
margin-top: 1rem;
}
.hilfe-hint strong {
color: var(--color-text);
}
/* ── Warn-Box ── */
.hilfe-warn {
background: rgba(231,76,60,0.08);
border-left: 3px solid #e74c3c;
border-radius: 0 8px 8px 0;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.6;
margin-top: 1rem;
}
.hilfe-warn strong {
color: #e74c3c;
}
/* ── Info-Box (neutral) ── */
.hilfe-info {
background: var(--color-secondary);
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: var(--color-muted);
line-height: 1.6;
margin-top: 1rem;
}
/* ── Einfache Tabelle ── */
.hilfe-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
margin-top: 1rem;
}
.hilfe-table th {
text-align: left;
color: var(--color-text);
font-weight: 600;
padding: 0.4rem 0.75rem 0.4rem 0;
border-bottom: 1px solid var(--color-secondary);
}
.hilfe-table td {
color: var(--color-muted);
padding: 0.5rem 0.75rem 0.5rem 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
vertical-align: top;
line-height: 1.5;
}
.hilfe-table tr:last-child td {
border-bottom: none;
}
/* ── Trennlinie ── */
.hilfe-divider {
border: none;
border-top: 1px solid var(--color-secondary);
margin: 1.25rem 0 0;
}
/* ── Inline-Badge ── */
.hilfe-badge {
display: inline-block;
background: var(--color-secondary);
border-radius: 4px;
padding: 0.1rem 0.45rem;
font-size: 0.78rem;
font-weight: 600;
color: var(--color-muted);
vertical-align: middle;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<!-- ── Kopfzeile ─────────────────────────────────────── -->
<div class="hilfe-header">
<h1>🔒 TTLock</h1>
<p>Hilfe zur Einrichtung der Kommunikation mit einer TTLock-Schlüsselbox</p>
</div>
<!-- ── Abschnitt: Einfacher Fließtext ───────────────── -->
<div class="hilfe-section open" id="sec-intro">
<div class="hilfe-section-header" onclick="toggleSection('sec-intro')">
<span class="hilfe-section-title">📖 Was ist das?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
<a href="https://ttlock.com/#/">TTLock</a> ist ein weit verbreitetes System für die Verwaltung von smarten Schlössern und Schlüsselboxen. Die Hardware kommuniziert in der Regel via Bluetooth, lässt sich aber über ein G2-Gateway auch aus der Ferne steuern.
</p>
<p>
Für Entwickler und Unternehmen bietet die TTLock Open Platform eine leistungsstarke REST-API. Damit lässt sich die Schlossverwaltung in eigene Anwendungen integrieren.
Diese API verwenden wir, um die Codes deiner Schlüsselbox zu steuern - kein nerviges manuelles Eintragen des generierten Schlüssels mehr.
</p>
<p>
TTLock steht allen Premium-Abonenten zur Verfügung.
</p>
</div>
</div>
<!-- ── Abschnitt: Tabelle ────────────────────────────── -->
<div class="hilfe-section" id="sec-table">
<div class="hilfe-section-header" onclick="toggleSection('sec-table')">
<span class="hilfe-section-title">📋 Voraussetzungen</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<div class="hilfe-warn">
<strong>Achtung:</strong> Für die Verwendung ist zwingend ein G2-Gateway für die Kommunikation von TTLock-Server zu Deiner Schlüsselbox notwendig.
</div>
<div class="hilfe-hint">
<strong>Hinweis:</strong> Für die Verwendung einer TTLock-Schlüsselbox in Spielen ist zwingend ein Premium-Abonement notwendig.
</div>
</div>
</div>
<!-- ── Abschnitt: Schritt-für-Schritt ───────────────── -->
<div class="hilfe-section" id="sec-howto">
<div class="hilfe-section-header" onclick="toggleSection('sec-howto')">
<span class="hilfe-section-title">🚀 So funktioniert es</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>Führe diese Schritte der Reihe nach aus:</p>
<ol class="hilfe-steps">
<li>
<span class="step-num">1</span>
<span>App-Setup: Verbinde deine Schlüsselbox in der TTLock-App. Wichtig: Für die Fernsteuerung muss ein Gateway (G2) eingerichtet und aktiv sein.</span>
</li>
<li>
<span class="step-num">2</span>
<span>Fernzugriff aktivieren: Aktiviere die Funktion in den App-Einstellungen. Tipp: Schalte das WLAN an deinem Handy aus und versuche, die Box über mobile Daten zu öffnen. Funktioniert das? Dann ist alles bereit.</span>
</li>
<li>
<span class="step-num">3</span>
<span>Accounts verknüpfen: Trage deine TTLock-Zugangsdaten unter Einstellungen &gt; TTLock ein.</span>
</li>
<li>
<span class="step-num">4</span>
<span>Lock-ID hinterlegen: Gib die ID deiner Box an (zu finden unter MAC/ID). Wichtig: Nur den Teil hinter dem Schrägstrich nutzen (z.B. bei 00:11.../123456 nur 123456).</span>
</li>
<li>
<span class="step-num">5</span>
<span>Verbindung testen: Klicke auf „Verbindung testen“. Erst nach einem grünen Licht ist das System aktiv.</span>
</li>
</ol>
<!-- Hinweis-Box (lila) -->
<div class="hilfe-hint">
<strong>Hinweis:</strong> Wir speichern dein Passwort nicht im Klartext in der Datenbank sondern nur als MD5-Hash.
</div>
</div>
</div>
<!-- ── Abschnitt: FAQ ────────────────────────────────── -->
<div class="hilfe-section" id="sec-faq1">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq1')">
<span class="hilfe-section-title">❓ Warum steht dieser Dienst nur für Abonennten zur Verfügung?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Die Verwendung der API von TTLock ist nur begrenzt kostenlos verwendbar. Ab bestimmten Kontingenten wird die Verwendung für uns kostenpflichtig.
</p>
<div class="hilfe-info">
Es gilt weiter der Grundsatz, XXX-Sphere soll niemanden reich machen - Die Abonemments dienen dazu die laufenden Kosten (Server, API-Schnittstellen etc.) zu decken.
</div>
</div>
</div>
<div class="hilfe-section" id="sec-faq2">
<div class="hilfe-section-header" onclick="toggleSection('sec-faq2')">
<span class="hilfe-section-title">❓ Hilfe - ich komme nicht mehr raus...Was machen Sachen?</span>
<span class="hilfe-section-arrow"></span>
</div>
<div class="hilfe-section-body">
<p>
Sollte sich der Schlüssel noch in der Box befinden und ihr nicht in einem aktiven Lock sein, besteht die Möglichkeit einen neuen Code für eine Notfallöffnung zu generieren:
</p>
<ol class="hilfe-steps">
<li>
<span class="step-num">1</span>
<span>Öffne die Einstellungen</span>
</li>
<li>
<span class="step-num">2</span>
<span>Navigiere zum Bereich '🔒 TTLock'</span>
</li>
<li>
<span class="step-num">3</span>
<span>Drücke '🔒 Öffnen'</span>
</li>
</ol>
<p>
Der temporäre Code zum Öffnen wird euch angezeigt - damit lässt sich die Box dann öffnen.
</p>
</div>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script>
<script>
function toggleSection(id) {
document.getElementById(id).classList.toggle('open');
}
function openFromHash() {
const hash = window.location.hash.slice(1);
if (!hash) return;
const el = document.getElementById(hash);
if (el && el.classList.contains('hilfe-section')) {
el.classList.add('open');
setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
}
}
document.addEventListener('DOMContentLoaded', openFromHash);
window.addEventListener('hashchange', openFromHash);
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xXx Games</title> <title>xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Info XXX The Game</title> <title>BDSM Game Info xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chastity Game Info XXX The Game</title> <title>Chastity Game Info xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vanilla Game Info XXX The Game</title> <title>Vanilla Game Info xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lock-Einladung XXX The Game</title> <title>Lock-Einladung xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -54,6 +54,7 @@ window.ICONS = {
SETTINGS: { type: 'emoji', value: '⚙️' }, SETTINGS: { type: 'emoji', value: '⚙️' },
LOGOUT: { type: 'emoji', value: '⏏️' }, LOGOUT: { type: 'emoji', value: '⏏️' },
PROFILE: { type: 'emoji', value: '👤' }, PROFILE: { type: 'emoji', value: '👤' },
HELP: { type: 'emoji', value: '❓' },
// ── Aufgaben / Items ────────────────────────────────────────────────── // ── Aufgaben / Items ──────────────────────────────────────────────────
TOYS: { type: 'emoji', value: '➰' }, TOYS: { type: 'emoji', value: '➰' },

View File

@@ -77,25 +77,36 @@
const adminCls = path === '/admin.html' ? ' class="active"' : ''; const adminCls = path === '/admin.html' ? ' class="active"' : '';
const adminItem = `<li id="navAdminLink" style="display:none"><a href="/admin.html"${adminCls}><span class="icon">${I('ADMIN') || '⚙'}</span> Administration</a></li>`; const adminItem = `<li id="navAdminLink" style="display:none"><a href="/admin.html"${adminCls}><span class="icon">${I('ADMIN') || '⚙'}</span> Administration</a></li>`;
const footerLinks = [
{ href: '/help/kontakt.html', icon: '✉️', label: 'Kontakt & Feedback' },
{ href: '/help/impressum.html', icon: '📄', label: 'Impressum' },
];
const footerNav = footerLinks.map(({ href, icon, label }) => {
const cls = path === href ? ' class="active"' : '';
return `<li><a href="${href}"${cls}><span class="icon">${icon}</span> ${label}</a></li>`;
}).join('');
document.body.insertAdjacentHTML('afterbegin', ` document.body.insertAdjacentHTML('afterbegin', `
<div class="sidebar-overlay" id="sidebarOverlay"></div> <div class="sidebar-overlay" id="sidebarOverlay"></div>
<button class="burger" id="burgerBtn" aria-label="Menü öffnen"> <button class="burger" id="burgerBtn" aria-label="Menü öffnen">
<span class="burger-icon"><span></span><span></span><span></span></span> <span class="burger-icon"><span></span><span></span><span></span></span>
</button> </button>
<aside class="sidebar" id="sidebar"> <div class="sidebar-wrapper" id="sidebar">
<div class="sidebar-logo-area"> <aside class="sidebar">
<a href="/userhome.html"><img src="/img/logo.png" alt="Logo"></a> <div class="sidebar-scroll-area">
<ul>
${socialNav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${nav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;" id="navAdminDivider" style="display:none"></li>
${adminItem}
</ul>
</div>
</aside>
<div class="sidebar-footer">
<ul>${footerNav}</ul>
</div> </div>
<ul> </div>
${homeItem}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${socialNav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;"></li>
${nav}
<li><hr style="border:none;border-top:1px solid var(--color-secondary);margin:0.4rem 1rem;" id="navAdminDivider" style="display:none"></li>
${adminItem}
</ul>
</aside>
`); `);
// Sidebar und .main in einen zentrierten App-Wrapper verschieben // Sidebar und .main in einen zentrierten App-Wrapper verschieben

View File

@@ -25,7 +25,9 @@
topbar.className = 'topbar'; topbar.className = 'topbar';
topbar.id = 'topbar'; topbar.id = 'topbar';
topbar.innerHTML = ` topbar.innerHTML = `
<div class="topbar-left"></div> <div class="topbar-left">
<a href="/userhome.html"><img class="topbar-banner" src="/img/banner.png" alt="xXx Sphere"></a>
</div>
<div class="topbar-search-wrap"> <div class="topbar-search-wrap">
<span class="topbar-search-icon">${IC('SEARCH')}</span> <span class="topbar-search-icon">${IC('SEARCH')}</span>
<input type="text" id="topbarSearchInput" placeholder="Suchen…" autocomplete="off" spellcheck="false"> <input type="text" id="topbarSearchInput" placeholder="Suchen…" autocomplete="off" spellcheck="false">
@@ -100,6 +102,10 @@
<a href="/einstellungen.html" class="topbar-profile-link"> <a href="/einstellungen.html" class="topbar-profile-link">
<span>${IC('SETTINGS')}</span> Einstellungen <span>${IC('SETTINGS')}</span> Einstellungen
</a> </a>
<a href="/help/overview.html" class="topbar-profile-link">
<span>${IC('HELP')}</span> Hilfe
</a>
<hr style="border:none;border-top:1px solid var(--color-secondary);margin:0;">
<a href="/login/logout" class="topbar-profile-link topbar-profile-link--danger"> <a href="/login/logout" class="topbar-profile-link topbar-profile-link--danger">
<span>${IC('LOGOUT')}</span> Abmelden <span>${IC('LOGOUT')}</span> Abmelden
</a> </a>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyholder*In bestätigt XXX The Game</title> <title>Keyholder*In bestätigt xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyholder XXX The Game</title> <title>Keyholder xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xXx Games Login</title> <title>Login xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meine Locks XXX The Game</title> <title>Meine Locks xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -61,7 +61,26 @@
.form-hint { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; } .form-hint { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
.form-row input[type="text"], .form-row input[type="text"],
.form-row input[type="number"], .form-row input[type="number"],
.form-row select { width:100%; box-sizing:border-box; } .form-row select {
width:100%; box-sizing:border-box;
padding:0.65rem 0.9rem;
border:1px solid var(--color-secondary);
border-radius:6px;
background:var(--color-secondary);
color:var(--color-text);
font-size:1rem;
outline:none;
appearance:none;
-webkit-appearance:none;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat:no-repeat;
background-position:right 0.9rem center;
padding-right:2.2rem;
cursor:pointer;
transition:border-color 0.15s;
}
.form-row select:focus { border-color:var(--color-primary); }
.form-row select option { background:var(--color-card); }
.checkbox-row { .checkbox-row {
display:flex; align-items:center; gap:0.6rem; display:flex; align-items:center; gap:0.6rem;
margin-bottom:0.6rem; cursor:pointer; margin-bottom:0.6rem; cursor:pointer;
@@ -157,8 +176,19 @@
background:var(--color-card); border-radius:7px; padding:0.55rem 0.75rem; background:var(--color-card); border-radius:7px; padding:0.55rem 0.75rem;
flex-wrap:wrap; flex-wrap:wrap;
} }
.wheel-item select { flex:1; min-width:150px; box-sizing:border-box; } .wheel-item select {
.wheel-item input[type="number"] { width:80px; box-sizing:border-box; } flex:1; min-width:150px; box-sizing:border-box;
padding:0.65rem 0.9rem; padding-right:2.2rem;
border:1px solid var(--color-secondary); border-radius:6px;
background:var(--color-secondary); color:var(--color-text);
font-size:1rem; outline:none;
appearance:none; -webkit-appearance:none;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:right 0.9rem center;
cursor:pointer; transition:border-color 0.15s;
}
.wheel-item select:focus { border-color:var(--color-primary); }
.wheel-item select option { background:var(--color-card); }
.wheel-item input[type="text"] { flex:1; min-width:120px; box-sizing:border-box; } .wheel-item input[type="text"] { flex:1; min-width:120px; box-sizing:border-box; }
.btn-remove { background:none; border:none; color:rgba(200,50,50,0.7); cursor:pointer; font-size:0.95rem; padding:0; margin:0; width:auto; } .btn-remove { background:none; border:none; color:rgba(200,50,50,0.7); cursor:pointer; font-size:0.95rem; padding:0; margin:0; width:auto; }
@@ -290,6 +320,20 @@
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label> <label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
</div> </div>
</div> </div>
<!-- Aufgaben (CardLock) -->
<div class="form-section">
<div class="form-section-title">Aufgaben (optional)</div>
<div id="sectionCardTaskMode" style="display:none;margin-bottom:0.65rem;">
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
<div class="radio-group">
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
</div>
</div>
<div class="task-list" id="modalCardTaskList"></div>
<button class="btn-add" onclick="addTask()">+ Aufgabe hinzufügen</button>
</div>
</div> </div>
<!-- ══ Zeit-Lock Bereich ══ --> <!-- ══ Zeit-Lock Bereich ══ -->
@@ -324,24 +368,32 @@
</div> </div>
</div> </div>
<!-- Spinning Wheel --> <!-- Glücksrad -->
<div class="form-section"> <div class="form-section">
<div class="form-section-title">Spinning Wheel (optional)</div> <div class="form-section-title">Glücksrad (optional)</div>
<div class="wheel-list" id="wheelList"></div> <div class="checkbox-row">
<button class="btn-add" onclick="addWheelEntry()">+ Eintrag hinzufügen</button> <input type="checkbox" id="fSpinToggle" onchange="toggleWheel(this.checked)">
<div class="form-row" id="rowSpinsEvery" style="margin-top:0.75rem;display:none;"> <label for="fSpinToggle">Glücksrad aktivieren</label>
<label>Rad drehen alle</label> </div>
<div class="time-picker"> <div id="wheelFields" style="display:none;">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'d')"></button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',1,'d')">+</button></div><span class="tp-label">Tage</span></div> <div class="form-row" style="margin-top:0.5rem;">
<div class="tp-colon">:</div> <label>Rad drehen alle</label>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')"></button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Std</span></div> <div class="time-picker">
<div class="tp-colon">:</div> <div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'d')"></button><input type="text" id="se_d" value="0" readonly><button type="button" onclick="tpChange('se',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')"></button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Min</span></div> <div class="tp-colon">:</div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'h')"></button><input type="text" id="se_h" value="01" readonly><button type="button" onclick="tpChange('se',1,'h')">+</button></div><span class="tp-label">Std</span></div>
<div class="tp-colon">:</div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('se',-1,'m')"></button><input type="text" id="se_m" value="00" readonly><button type="button" onclick="tpChange('se',1,'m')">+</button></div><span class="tp-label">Min</span></div>
</div>
</div> </div>
<div class="form-row" style="margin-top:0.5rem;margin-bottom:0;"> <div class="form-row">
<label>Mindestdrehungen pro Tag (optional)</label> <label>Mindestdrehungen pro Tag (optional)</label>
<div class="inline-row"><input type="number" id="fMinSpins" min="1" placeholder=""> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div> <div class="inline-row"><input type="number" id="fMinSpins" min="1" placeholder=""> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
</div> </div>
<div style="margin-top:0.5rem;">
<div class="wheel-list" id="wheelList"></div>
<button class="btn-add" onclick="addWheelEntry()">+ Eintrag hinzufügen</button>
</div>
</div> </div>
</div> </div>
@@ -354,7 +406,7 @@
</div> </div>
<div id="taskTimingFields" style="display:none;"> <div id="taskTimingFields" style="display:none;">
<div class="form-row" style="margin-top:0.5rem;"> <div class="form-row" style="margin-top:0.5rem;">
<label>Aufgabe alle</label> <label>Aufgaben alle</label>
<div class="time-picker"> <div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'d')"></button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',1,'d')">+</button></div><span class="tp-label">Tage</span></div> <div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('te',-1,'d')"></button><input type="text" id="te_d" value="0" readonly><button type="button" onclick="tpChange('te',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-colon">:</div> <div class="tp-colon">:</div>
@@ -367,6 +419,19 @@
<label>Mindestaufgaben pro Tag (optional)</label> <label>Mindestaufgaben pro Tag (optional)</label>
<div class="inline-row"><input type="number" id="fMinTasks" min="1" placeholder=""> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div> <div class="inline-row"><input type="number" id="fMinTasks" min="1" placeholder=""> <span style="font-size:0.88rem;color:var(--color-text);">pro Tag</span></div>
</div> </div>
<div style="margin-top:0.85rem;">
<div id="sectionTaskMode" style="margin-bottom:0.65rem;">
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgaben?</div>
<div class="radio-group">
<label><input type="radio" name="modalTaskMode" value="RANDOM" checked> Zufall</label>
<label><input type="radio" name="modalTaskMode" value="KEYHOLDER" > Keyholder*In</label>
<label><input type="radio" name="modalTaskMode" value="COMMUNITY" > Community</label>
</div>
</div>
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.4rem;">Aufgaben <span style="color:#e74c3c;font-size:0.85em;">*</span></div>
<div class="task-list" id="modalTaskList"></div>
<button class="btn-add" onclick="addTask()">+ Aufgabe hinzufügen</button>
</div>
</div> </div>
</div> </div>
@@ -383,8 +448,14 @@
</select> </select>
</div> </div>
<div class="form-row" id="rowPenaltyValue" style="display:none;"> <div class="form-row" id="rowPenaltyValue" style="display:none;">
<label id="lblPenaltyValue">Dauer (Minuten)</label> <label>Dauer</label>
<div class="inline-row"><input type="number" id="fPenaltyValue" min="1" placeholder="Minuten"></div> <div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'d')"></button><input type="text" id="pv_d" value="0" readonly><button type="button" onclick="tpChange('pv',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-colon">:</div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'h')"></button><input type="text" id="pv_h" value="01" readonly><button type="button" onclick="tpChange('pv',1,'h')">+</button></div><span class="tp-label">Std</span></div>
<div class="tp-colon">:</div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('pv',-1,'m')"></button><input type="text" id="pv_m" value="00" readonly><button type="button" onclick="tpChange('pv',1,'m')">+</button></div><span class="tp-label">Min</span></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -420,23 +491,6 @@
</div> </div>
</div> </div>
<!-- Aufgaben-Modus (wenn Aufgaben vorhanden) -->
<div id="sectionTaskMode" style="display:none;" class="form-section">
<div class="form-section-title">Wer entscheidet über die Aufgabe?</div>
<div class="radio-group">
<label><input type="radio" name="modalTaskMode" value="RANDOM" checked> Zufall</label>
<label><input type="radio" name="modalTaskMode" value="KEYHOLDER" > Keyholder*In</label>
<label><input type="radio" name="modalTaskMode" value="COMMUNITY" > Community</label>
</div>
</div>
<!-- Aufgaben (immer, optional) -->
<div class="form-section">
<div class="form-section-title">Aufgaben (optional)</div>
<div class="task-list" id="modalTaskList"></div>
<button class="btn-add" onclick="addTask()">+ Aufgabe hinzufügen</button>
</div>
<div class="error-msg" id="modalError"></div> <div class="error-msg" id="modalError"></div>
<div id="modalDiscardConfirm" style="display:none;background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.3);border-radius:8px;padding:0.6rem 0.9rem;margin-bottom:0.5rem;display:none;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap;"> <div id="modalDiscardConfirm" style="display:none;background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.3);border-radius:8px;padding:0.6rem 0.9rem;margin-bottom:0.5rem;display:none;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap;">
@@ -600,22 +654,28 @@
function addWheelEntry(data) { function addWheelEntry(data) {
const id = ++wheelCtr; const id = ++wheelCtr;
const type = data?.type || 'ADD_TIME'; const type = data?.type || 'ADD_TIME';
const def = WHEEL_TYPES.find(t => t.value === type) || WHEEL_TYPES[0];
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'wheel-item'; div.className = 'wheel-item';
div.id = 'we-' + id; div.id = 'we-' + id;
div.innerHTML = buildWheelItemHtml(id, type, data?.intVal, data?.stringVal); div.innerHTML = buildWheelItemHtml(id, type, data?.intVal, data?.stringVal);
document.getElementById('wheelList').appendChild(div); document.getElementById('wheelList').appendChild(div);
tpFromMinutes('wt' + id, data?.intVal || 60);
updateWheelFields(id); updateWheelFields(id);
updateWheelTimingVisibility();
} }
function buildWheelItemHtml(id, type, intVal, stringVal) { function buildWheelItemHtml(id, type, intVal, stringVal) {
const opts = WHEEL_TYPES.map(t => const opts = WHEEL_TYPES.map(t =>
`<option value="${t.value}" ${t.value===type?'selected':''}>${esc(t.label)}</option>` `<option value="${t.value}" ${t.value===type?'selected':''}>${esc(t.label)}</option>`
).join(''); ).join('');
return `<select onchange="updateWheelFields(${id})">${opts}</select> return `<select onchange="updateWheelFields(${id})">${opts}</select>
<input type="number" id="we-int-${id}" placeholder="Min." min="1" <div id="we-tp-${id}" style="display:none;">
value="${intVal||''}" style="display:none"> <div class="time-picker">
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'d')"></button><input type="text" id="wt${id}_d" value="0" readonly><button type="button" onclick="tpChange('wt${id}',1,'d')">+</button></div><span class="tp-label">Tage</span></div>
<div class="tp-colon">:</div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'h')"></button><input type="text" id="wt${id}_h" value="01" readonly><button type="button" onclick="tpChange('wt${id}',1,'h')">+</button></div><span class="tp-label">Std</span></div>
<div class="tp-colon">:</div>
<div class="tp-seg"><div class="tp-seg-row"><button type="button" onclick="tpChange('wt${id}',-1,'m')"></button><input type="text" id="wt${id}_m" value="00" readonly><button type="button" onclick="tpChange('wt${id}',1,'m')">+</button></div><span class="tp-label">Min</span></div>
</div>
</div>
<input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200" <input type="text" id="we-str-${id}" placeholder="Text…" maxlength="200"
value="${esc(stringVal||'')}" style="display:none"> value="${esc(stringVal||'')}" style="display:none">
<button class="btn-remove" onclick="removeWheelEntry(${id})" title="Entfernen">✕</button>`; <button class="btn-remove" onclick="removeWheelEntry(${id})" title="Entfernen">✕</button>`;
@@ -624,27 +684,32 @@
const sel = document.querySelector(`#we-${id} select`); const sel = document.querySelector(`#we-${id} select`);
if (!sel) return; if (!sel) return;
const def = WHEEL_TYPES.find(t => t.value === sel.value) || WHEEL_TYPES[0]; const def = WHEEL_TYPES.find(t => t.value === sel.value) || WHEEL_TYPES[0];
document.getElementById('we-int-' + id).style.display = def.hasInt ? '' : 'none'; document.getElementById('we-tp-' + id).style.display = def.hasInt ? '' : 'none';
document.getElementById('we-str-' + id).style.display = def.hasStr ? '' : 'none'; document.getElementById('we-str-' + id).style.display = def.hasStr ? '' : 'none';
if (def.intLabel) document.getElementById('we-int-' + id).placeholder = def.intLabel;
if (def.strLabel) document.getElementById('we-str-' + id).placeholder = def.strLabel;
} }
function removeWheelEntry(id) { function removeWheelEntry(id) {
document.getElementById('we-' + id)?.remove(); document.getElementById('we-' + id)?.remove();
updateWheelTimingVisibility();
} }
function updateWheelTimingVisibility() { function toggleWheel(on) {
const hasEntries = document.querySelectorAll('.wheel-item').length > 0; document.getElementById('wheelFields').style.display = on ? '' : 'none';
document.getElementById('rowSpinsEvery').style.display = hasEntries ? '' : 'none'; if (!on) {
tpFromMinutes('se', 60);
document.getElementById('fMinSpins').value = '';
document.getElementById('wheelList').innerHTML = '';
}
} }
function collectWheelEntries() { function collectWheelEntries() {
return Array.from(document.querySelectorAll('.wheel-item')).map(item => { return Array.from(document.querySelectorAll('#wheelList .wheel-item')).map(item => {
const id = item.id.replace('we-',''); const id = item.id.replace('we-','');
const sel = item.querySelector('select'); const sel = item.querySelector('select');
const def = WHEEL_TYPES.find(t => t.value === sel?.value); const def = WHEEL_TYPES.find(t => t.value === sel?.value);
const d = parseInt(document.getElementById('wt' + id + '_d')?.value) || 0;
const h = parseInt(document.getElementById('wt' + id + '_h')?.value) || 0;
const m = parseInt(document.getElementById('wt' + id + '_m')?.value) || 0;
const minutes = d * 1440 + h * 60 + m;
return { return {
type: sel?.value, type: sel?.value,
intVal: def?.hasInt ? (parseInt(document.getElementById('we-int-'+id)?.value)||null) : null, intVal: def?.hasInt ? (minutes > 0 ? minutes : null) : null,
stringVal: def?.hasStr ? (document.getElementById('we-str-'+id)?.value||null) : null, stringVal: def?.hasStr ? (document.getElementById('we-str-'+id)?.value||null) : null,
}; };
}); });
@@ -653,7 +718,11 @@
// ── Aufgaben-Timing (TimeLock) ── // ── Aufgaben-Timing (TimeLock) ──
function toggleTaskTiming(on) { function toggleTaskTiming(on) {
document.getElementById('taskTimingFields').style.display = on ? '' : 'none'; document.getElementById('taskTimingFields').style.display = on ? '' : 'none';
if (!on) { tpFromMinutes('te', 480); document.getElementById('fMinTasks').value = ''; } if (!on) {
tpFromMinutes('te', 480);
document.getElementById('fMinTasks').value = '';
document.getElementById('modalTaskList').innerHTML = '';
}
} }
// ── Strafmaß ── // ── Strafmaß ──
@@ -661,8 +730,6 @@
const type = document.getElementById('fPenaltyType').value; const type = document.getElementById('fPenaltyType').value;
const needsVal = type === 'ADD' || type === 'FREEZE'; const needsVal = type === 'ADD' || type === 'FREEZE';
document.getElementById('rowPenaltyValue').style.display = needsVal ? '' : 'none'; document.getElementById('rowPenaltyValue').style.display = needsVal ? '' : 'none';
document.getElementById('lblPenaltyValue').textContent =
type === 'ADD' ? 'Minuten hinzufügen' : 'Einfrieren für (Minuten)';
} }
// ── Aufgaben ── // ── Aufgaben ──
@@ -682,16 +749,18 @@
<button class="btn-remove" onclick="removeTask(${id})" title="Entfernen">✕</button> <button class="btn-remove" onclick="removeTask(${id})" title="Entfernen">✕</button>
</div> </div>
<textarea placeholder="Beschreibung (optional)…" maxlength="600" id="mt-desc-${id}">${descVal}</textarea>`; <textarea placeholder="Beschreibung (optional)…" maxlength="600" id="mt-desc-${id}">${descVal}</textarea>`;
document.getElementById('modalTaskList').appendChild(div); const containerId = currentModalType() === 'CARDLOCK' ? 'modalCardTaskList' : 'modalTaskList';
document.getElementById(containerId).appendChild(div);
updateTaskModeVisibility(); updateTaskModeVisibility();
} }
function removeTask(id) { document.getElementById('mt-'+id)?.remove(); updateTaskModeVisibility(); } function removeTask(id) { document.getElementById('mt-'+id)?.remove(); updateTaskModeVisibility(); }
function updateTaskModeVisibility() { function updateTaskModeVisibility() {
const hasTasks = document.querySelectorAll('.task-item').length > 0;
const type = currentModalType(); const type = currentModalType();
// For CardLock: show mode only when task cards > 0 OR tasks exist if (type === 'CARDLOCK') {
// For TimeLock: show mode when tasks exist OR timing is active const hasCardTasks = document.querySelectorAll('#modalCardTaskList .task-item').length > 0;
document.getElementById('sectionTaskMode').style.display = hasTasks ? '' : 'none'; document.getElementById('sectionCardTaskMode').style.display = hasCardTasks ? '' : 'none';
}
// For TimeLock: sectionTaskMode is always visible when taskTimingFields is open
} }
function collectTasks() { function collectTasks() {
return Array.from(document.querySelectorAll('.task-item')).map(item => { return Array.from(document.querySelectorAll('.task-item')).map(item => {
@@ -735,7 +804,8 @@
document.getElementById('modalError').style.display = 'none'; document.getElementById('modalError').style.display = 'none';
document.getElementById('modalSaveBtn').disabled = false; document.getElementById('modalSaveBtn').disabled = false;
document.getElementById('modalTaskList').innerHTML = ''; document.getElementById('modalTaskList').innerHTML = '';
document.getElementById('wheelList').innerHTML = ''; document.getElementById('fSpinToggle').checked = false;
toggleWheel(false);
document.getElementById('errGreen').style.display = 'none'; document.getElementById('errGreen').style.display = 'none';
taskCtr = 0; wheelCtr = 0; taskCtr = 0; wheelCtr = 0;
@@ -768,11 +838,15 @@
tpFromMinutes('tmax', template?.maxTimeInMinutes || 60); tpFromMinutes('tmax', template?.maxTimeInMinutes || 60);
document.getElementById('fEndTimeVisible').checked = template?.endTimeVisible || false; document.getElementById('fEndTimeVisible').checked = template?.endTimeVisible || false;
// Spinning Wheel // Glücksrad
(template?.spinningWheelEntries || []).forEach(e => addWheelEntry(e)); const hasWheelEntries = !!(template?.spinningWheelEntries?.length);
updateWheelTimingVisibility(); document.getElementById('fSpinToggle').checked = hasWheelEntries;
if (template?.spinsEveryMinutes) tpFromMinutes('se', template.spinsEveryMinutes); toggleWheel(hasWheelEntries);
document.getElementById('fMinSpins').value = template?.minSpinsPerDay || ''; if (hasWheelEntries) {
template.spinningWheelEntries.forEach(e => addWheelEntry(e));
if (template.spinsEveryMinutes) tpFromMinutes('se', template.spinsEveryMinutes);
document.getElementById('fMinSpins').value = template.minSpinsPerDay || '';
}
// Aufgaben-Timing // Aufgaben-Timing
const hasTaskTiming = !!template?.taskEveryMinutes; const hasTaskTiming = !!template?.taskEveryMinutes;
@@ -785,7 +859,7 @@
// Strafmaß // Strafmaß
document.getElementById('fPenaltyType').value = template?.penaltyType || ''; document.getElementById('fPenaltyType').value = template?.penaltyType || '';
document.getElementById('fPenaltyValue').value = template?.penaltyValue || ''; tpFromMinutes('pv', template?.penaltyValue || 60);
onPenaltyTypeChange(); onPenaltyTypeChange();
} }
@@ -798,7 +872,8 @@
// Aufgaben // Aufgaben
(template?.tasks||[]).forEach(t => addTask(t)); (template?.tasks||[]).forEach(t => addTask(t));
const mode = template?.taskMode || template?.taskCardMode || 'RANDOM'; const mode = template?.taskMode || template?.taskCardMode || 'RANDOM';
const radioEl = document.querySelector(`input[name="modalTaskMode"][value="${mode}"]`); const radioName = type === 'CARDLOCK' ? 'modalCardTaskMode' : 'modalTaskMode';
const radioEl = document.querySelector(`input[name="${radioName}"][value="${mode}"]`);
if (radioEl) radioEl.checked = true; if (radioEl) radioEl.checked = true;
updateTaskModeVisibility(); updateTaskModeVisibility();
@@ -853,6 +928,7 @@
// ── Speichern ── // ── Speichern ──
async function saveTemplate() { async function saveTemplate() {
try {
document.getElementById('modalError').style.display = 'none'; document.getElementById('modalError').style.display = 'none';
clearErr('rowName'); clearErr('rowName');
const type = currentModalType(); const type = currentModalType();
@@ -915,7 +991,7 @@
hygineOpeningEveryMinites: hygieneEvery, hygineOpeningEveryMinites: hygieneEvery,
hygineOpeningDurationMinutes: hygieneDur, hygineOpeningDurationMinutes: hygieneDur,
tasks, requiresVerification: document.getElementById('fRequiresVerification').checked, tasks, requiresVerification: document.getElementById('fRequiresVerification').checked,
taskMode: document.querySelector('input[name="modalTaskMode"]:checked')?.value||'RANDOM', taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
}; };
} else { } else {
// TimeLock // TimeLock
@@ -937,14 +1013,14 @@
let spinsEvery = null, minSpinsPerDay = null; let spinsEvery = null, minSpinsPerDay = null;
if (wheelEntries.length > 0) { if (wheelEntries.length > 0) {
spinsEvery = tpToMinutes('se'); spinsEvery = tpToMinutes('se');
if (spinsEvery < 1) { showModalError('Spinning-Wheel-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); } if (spinsEvery < 1) { showModalError('Glücksrad-Intervall muss mindestens 1 Minute betragen.'); firstError=firstError||document.getElementById('modalError'); }
const ms = parseInt(document.getElementById('fMinSpins').value); const ms = parseInt(document.getElementById('fMinSpins').value);
minSpinsPerDay = isNaN(ms)||ms<1 ? null : ms; minSpinsPerDay = isNaN(ms)||ms<1 ? null : ms;
} }
// Validierung: Aufgaben-Timing + TASK-Wheel-Eintrag schließen sich aus // Validierung: Aufgaben-Timing + TASK-Wheel-Eintrag schließen sich aus
if (hasTaskTiming && wheelEntries.some(e => e.type === 'TASK')) { if (hasTaskTiming && wheelEntries.some(e => e.type === 'TASK')) {
showModalError('Aufgaben-Timing kann nicht mit TASK-Einträgen im Spinning-Wheel kombiniert werden.'); showModalError('Aufgaben-Timing kann nicht mit TASK-Einträgen im Glücksrad kombiniert werden.');
firstError = firstError || document.getElementById('modalError'); firstError = firstError || document.getElementById('modalError');
} }
@@ -965,17 +1041,17 @@
if (spinsEvery && minSpinsPerDay) { if (spinsEvery && minSpinsPerDay) {
const minSpinTime = spinsEvery * minSpinsPerDay; const minSpinTime = spinsEvery * minSpinsPerDay;
if (minSpinTime > 24 * 60) { if (minSpinTime > 24 * 60) {
showModalError('Spinning-Wheel-Konfiguration erfordert mehr als 24 Stunden pro Tag bitte Intervall oder Min.-Anzahl reduzieren.'); showModalError('Glücksrad-Konfiguration erfordert mehr als 24 Stunden pro Tag bitte Intervall oder Min.-Anzahl reduzieren.');
firstError = firstError || document.getElementById('modalError'); firstError = firstError || document.getElementById('modalError');
} else if (minSpinTime > 12 * 60) { } else if (minSpinTime > 12 * 60) {
showModalError('⚠ Warnung: Spinning-Wheel-Konfiguration erfordert mehr als 12 Stunden pro Tag.'); showModalError('⚠ Warnung: Glücksrad-Konfiguration erfordert mehr als 12 Stunden pro Tag.');
} }
} }
const penaltyType = document.getElementById('fPenaltyType').value || null; const penaltyType = document.getElementById('fPenaltyType').value || null;
const penaltyRaw = parseInt(document.getElementById('fPenaltyValue').value); const penaltyMinutes = tpToMinutes('pv');
const penaltyValue = penaltyType && (penaltyType==='ADD'||penaltyType==='FREEZE') const penaltyValue = penaltyType && (penaltyType==='ADD'||penaltyType==='FREEZE')
? (isNaN(penaltyRaw)||penaltyRaw<1 ? null : penaltyRaw) : null; ? (penaltyMinutes < 1 ? null : penaltyMinutes) : null;
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; } if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
@@ -1003,6 +1079,11 @@
if (res.ok) { closeModal(); resetList(); } if (res.ok) { closeModal(); resetList(); }
else { showModalError('Fehler beim Speichern.'); btn.disabled=false; } else { showModalError('Fehler beim Speichern.'); btn.disabled=false; }
} catch(e) { btn.disabled=false; } } catch(e) { btn.disabled=false; }
} catch(e) {
console.error('saveTemplate exception:', e);
showModalError('Interner Fehler: ' + e.message);
document.getElementById('modalSaveBtn').disabled = false;
}
} }
// ── Löschen ── // ── Löschen ──

View File

@@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nachrichten XXX The Game</title> <title>Nachrichten xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -318,10 +318,12 @@
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen"></button> <button class="lightbox-close" onclick="closeLightbox()" aria-label="Schließen"></button>
</div> </div>
<script src="/js/icons.js"></script> <script src="/js/icons.js"></script>
<script src="/js/sidebar.js"></script> <script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script> <script src="/js/social-sidebar.js"></script>
<script> <script>
const SUPPORT_USER_ID = 'dbf1e35a-e331-3211-9889-d0d21f386028';
let myId = null; let myId = null;
let activePartnerId = null; let activePartnerId = null;
let pollTimer = null; let pollTimer = null;
@@ -411,8 +413,9 @@
avatarEl.style.display = 'none'; avatarEl.style.display = 'none';
} }
document.getElementById('threadInputWrap').style.display = ''; const isSupport = partnerId === SUPPORT_USER_ID;
document.getElementById('msgInput').focus(); document.getElementById('threadInputWrap').style.display = isSupport ? 'none' : '';
if (!isSupport) document.getElementById('msgInput').focus();
if (window.innerWidth <= 768) showThread(); if (window.innerWidth <= 768) showThread();

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BDSM Game Session einrichten XXX The Game</title> <title>BDSM Game Session einrichten xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neues Lock XXX The Game</title> <title>Neues Lock xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>
@@ -304,6 +304,16 @@
</div> </div>
</div> </div>
<!-- TTLock Lade-Overlay -->
<div class="modal-overlay" id="ttlLoadingOverlay">
<div class="modal-bg"></div>
<div class="modal-box" style="max-width:320px;text-align:center;gap:0.75rem;">
<div style="font-size:2rem;"></div>
<div style="font-weight:600;">TTLock-Kommunikation läuft…</div>
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
</div>
</div>
<!-- Entsperrcode-Modal --> <!-- Entsperrcode-Modal -->
<div class="modal-overlay" id="unlockModal"> <div class="modal-overlay" id="unlockModal">
<div class="modal-bg"></div> <div class="modal-bg"></div>
@@ -342,6 +352,7 @@
let comboActiveIdx = -1; let comboActiveIdx = -1;
let selectedLockControl = 'UNLOCK_CODE'; let selectedLockControl = 'UNLOCK_CODE';
let hasPaidSubscription = false; let hasPaidSubscription = false;
let ttlockReady = false;
// ── Boot ── // ── Boot ──
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => { fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
@@ -349,28 +360,35 @@
myUserId = user.userId; myUserId = user.userId;
myUserName = user.name; myUserName = user.name;
// Subscription + Templates parallel laden // Subscription + Templates + TTLock-Config parallel laden
try { try {
const [cardTpls, timeTpls, subData] = await Promise.all([ const [cardTpls, timeTpls, subData, ttlCfg] = await Promise.all([
fetch('/cardlock/templates').then(r => r.ok ? r.json() : []), fetch('/cardlock/templates').then(r => r.ok ? r.json() : []),
fetch('/timelock/templates').then(r => r.ok ? r.json() : []), fetch('/timelock/templates').then(r => r.ok ? r.json() : []),
fetch('/subscription/me').then(r => r.ok ? r.json() : null) fetch('/subscription/me').then(r => r.ok ? r.json() : null),
fetch('/user/me/ttlock').then(r => r.ok ? r.json() : null)
]); ]);
allTemplates = [ allTemplates = [
...cardTpls.map(t => ({ ...t, _type: 'cardlock' })), ...cardTpls.map(t => ({ ...t, _type: 'cardlock' })),
...timeTpls.map(t => ({ ...t, _type: 'timelock' })) ...timeTpls.map(t => ({ ...t, _type: 'timelock' }))
]; ];
hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM'); hasPaidSubscription = !!(subData && subData.subscriptionType === 'PREMIUM');
} catch { allTemplates = []; } ttlockReady = !!(ttlCfg && ttlCfg.testSuccessful);
const ttlockTestOk = ttlockReady;
if (hasPaidSubscription) { if (hasPaidSubscription && ttlockTestOk) {
const opt = document.getElementById('lcOptTtlock'); const opt = document.getElementById('lcOptTtlock');
opt.classList.remove('lc-disabled'); opt.classList.remove('lc-disabled');
opt.querySelector('input').disabled = false; opt.querySelector('input').disabled = false;
document.getElementById('lcTtlockBadge').style.display = 'none'; document.getElementById('lcTtlockBadge').style.display = 'none';
document.getElementById('lcTtlockDesc').textContent = document.getElementById('lcTtlockDesc').textContent =
'Steuert ein TTLock-Smartschloss direkt über die App-Integration.'; 'Steuert ein TTLock-Smartschloss direkt über die App-Integration.';
} } else if (hasPaidSubscription && !ttlockTestOk) {
document.getElementById('lcTtlockBadge').textContent = 'KONFIG';
document.getElementById('lcTtlockDesc').textContent =
'TTLock ist noch nicht konfiguriert. Bitte teste die Verbindung zuerst in den Einstellungen.';
}
} catch { allTemplates = []; }
if (allTemplates.length === 0) { if (allTemplates.length === 0) {
document.querySelector('.content').innerHTML = ` document.querySelector('.content').innerHTML = `
@@ -583,7 +601,7 @@
// ── LockControl-Auswahl ── // ── LockControl-Auswahl ──
function selectLockControl(type) { function selectLockControl(type) {
const ids = { UNLOCK_CODE: 'lcOptUnlockCode', TRUST: 'lcOptTrust', TTLOCK: 'lcOptTtlock' }; const ids = { UNLOCK_CODE: 'lcOptUnlockCode', TRUST: 'lcOptTrust', TTLOCK: 'lcOptTtlock' };
if (type === 'TTLOCK' && !hasPaidSubscription) return; if (type === 'TTLOCK' && (!hasPaidSubscription || !ttlockReady)) return;
selectedLockControl = type; selectedLockControl = type;
Object.entries(ids).forEach(([t, id]) => { Object.entries(ids).forEach(([t, id]) => {
const el = document.getElementById(id); const el = document.getElementById(id);
@@ -746,12 +764,18 @@
}; };
} }
if (selectedLockControl === 'TTLOCK') {
document.getElementById('ttlLoadingOverlay').classList.add('open');
}
const res = await fetch(endpoint, { const res = await fetch(endpoint, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
document.getElementById('ttlLoadingOverlay').classList.remove('open');
if (!res.ok) { if (!res.ok) {
const errData = await res.json().catch(() => ({})); const errData = await res.json().catch(() => ({}));
if (res.status === 409 && errData.error === 'active_lock_exists') { if (res.status === 409 && errData.error === 'active_lock_exists') {
@@ -797,8 +821,9 @@
const btn = document.getElementById('unlockModalBtn'); const btn = document.getElementById('unlockModalBtn');
btn.textContent = "🔒 Los geht's"; btn.textContent = "🔒 Los geht's";
btn.onclick = async () => { btn.onclick = async () => {
btn.disabled = true; btn.disabled = true;
btn.textContent = '⏳ Neuer PIN wird gesetzt…'; document.getElementById('unlockModal').classList.remove('open');
document.getElementById('ttlLoadingOverlay').classList.add('open');
try { try {
await fetch(`/keyholder/${lockType}/${lockId}/relock`, { method: 'POST' }); await fetch(`/keyholder/${lockType}/${lockId}/relock`, { method: 'POST' });
} catch { /* Fehler ignorieren Weiterleitung trotzdem */ } } catch { /* Fehler ignorieren Weiterleitung trotzdem */ }

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personen suchen XXX The Game</title> <title>Personen suchen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profil XXX The Game</title> <title>Profil xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xXx Games Neues Konto erstellen</title> <title>Neues Konto erstellen xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xXx Games Neues Passwort</title> <title>Neues Passwort xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vanilla Game Neue Session XXX The Game</title> <title>Vanilla Game Neue Session xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
</head> </head>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toys XXX The Game</title> <title>Toys xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code-Historie XXX The Game</title> <title>Code-Historie xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home XXX The Game</title> <title>Home xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css"> <link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<style> <style>