Wetier am Cahstity game gebasterln

This commit is contained in:
2026-03-17 19:51:51 +01:00
parent 97c6f0a131
commit aafc203407
130 changed files with 9356 additions and 4222 deletions

View File

@@ -13,7 +13,15 @@
"Read(//tmp/**)", "Read(//tmp/**)",
"Bash(cd:*)", "Bash(cd:*)",
"Bash(JAVA_HOME=\"/snap/eclipse/132/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.10.v20260205-0638/jre\"; JAVA_HOME=\"$JAVA_HOME\" PATH=\"$JAVA_HOME/bin:$PATH\" ./gradlew compileJava 2>&1 | grep -E \"error:|ERROR|warning:|WARN|BUILD\" | head -30)", "Bash(JAVA_HOME=\"/snap/eclipse/132/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.10.v20260205-0638/jre\"; JAVA_HOME=\"$JAVA_HOME\" PATH=\"$JAVA_HOME/bin:$PATH\" ./gradlew compileJava 2>&1 | grep -E \"error:|ERROR|warning:|WARN|BUILD\" | head -30)",
"Bash(JAVA_HOME=\"/snap/eclipse/132/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.10.v20260205-0638/jre\" PATH=\"$JAVA_HOME/bin:$PATH\" ./gradlew compileJava 2>&1 | grep -E \"error:|BUILD\" | head -30)" "Bash(JAVA_HOME=\"/snap/eclipse/132/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.10.v20260205-0638/jre\" PATH=\"$JAVA_HOME/bin:$PATH\" ./gradlew compileJava 2>&1 | grep -E \"error:|BUILD\" | head -30)",
"Bash(./gradlew compileJava -q 2>&1 | tail -5)",
"Bash(./gradlew compileJava -q 2>&1 | tail -20)",
"Bash(./gradlew compileJava -q 2>&1 | tail -10)",
"Bash(./gradlew compileJava 2>&1 | tail -10)",
"Bash(./gradlew compileJava 2>&1 | tail -20)",
"Bash(./gradlew compileJava 2>&1 | tail -15)",
"Bash(./gradlew compileJava -q 2>&1 | tail -30)",
"Bash(ls -lah /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/cardlock/*.java)"
] ]
} }
} }

View File

@@ -1,5 +1,5 @@
#Mon Mar 16 20:07:41 CET 2026 #Tue Mar 17 19:49:49 CET 2026
display=\:0 display=\:0
host=Mario-Linux host=Mario-Linux
process-id=73435 process-id=140659
user=mario user=mario

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[ { [ {
"version" : "9.4.1-20260316020012+0000", "version" : "9.4.1-20260317014409+0000",
"buildTime" : "20260316020012+0000", "buildTime" : "20260317014409+0000",
"commitId" : "bf53443ce3925ea6141adbe2d2fd96493865b6ee", "commitId" : "0a84d6751921709fbc7d552a50d9e3b34eb1930d",
"current" : false, "current" : false,
"snapshot" : true, "snapshot" : true,
"nightly" : false, "nightly" : false,
@@ -10,15 +10,15 @@
"rcFor" : "", "rcFor" : "",
"milestoneFor" : "", "milestoneFor" : "",
"broken" : false, "broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260316020012+0000-bin.zip", "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260317014409+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260316020012+0000-bin.zip.sha256", "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260317014409+0000-bin.zip.sha256",
"checksum" : "2e805c642bbf8b8ccf39cf855b202bed260018f7ca61c37408c7f4c32a36a3e2", "checksum" : "b0756f51c7876351c3c18ee7c0777bd2c6b2e18bf4bf31dc17d367310f6cce5c",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260316020012+0000-wrapper.jar.sha256", "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260317014409+0000-wrapper.jar.sha256",
"wrapperChecksum" : "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c" "wrapperChecksum" : "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
}, { }, {
"version" : "9.5.0-20260316005115+0000", "version" : "9.5.0-20260317004629+0000",
"buildTime" : "20260316005115+0000", "buildTime" : "20260317004629+0000",
"commitId" : "b2fdb4cec79e4c0b1b5c1bfeebc8d4b5fd65c690", "commitId" : "644201d0b1d42cde4ea13645efefbdbf004cd6b1",
"current" : false, "current" : false,
"snapshot" : true, "snapshot" : true,
"nightly" : true, "nightly" : true,
@@ -27,10 +27,10 @@
"rcFor" : "", "rcFor" : "",
"milestoneFor" : "", "milestoneFor" : "",
"broken" : false, "broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260316005115+0000-bin.zip", "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260317004629+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260316005115+0000-bin.zip.sha256", "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260317004629+0000-bin.zip.sha256",
"checksum" : "bde9dc37434e5942fdd1e2f9c6f9571fc649224053e691021e2e87ba5af4a2b7", "checksum" : "fa62a8b9f5ed9a718f7a8d9e0d3221ea5a8b15dd1f07c8edb72e871615c1eac0",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260316005115+0000-wrapper.jar.sha256", "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260317004629+0000-wrapper.jar.sha256",
"wrapperChecksum" : "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8" "wrapperChecksum" : "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8"
}, { }, {
"version" : "9.5.0-milestone-7", "version" : "9.5.0-milestone-7",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@
<section name="org.eclipse.epp.internal.mpc.ui.wizards.MarketplaceWizardDialog_dialogBounds.absolute"> <section name="org.eclipse.epp.internal.mpc.ui.wizards.MarketplaceWizardDialog_dialogBounds.absolute">
<item key="DIALOG_X_ORIGIN" value="26"/> <item key="DIALOG_X_ORIGIN" value="26"/>
<item key="DIALOG_Y_ORIGIN" value="23"/> <item key="DIALOG_Y_ORIGIN" value="23"/>
<item key="DIALOG_WIDTH" value="613"/> <item key="DIALOG_WIDTH" value="791"/>
<item key="DIALOG_HEIGHT" value="890"/> <item key="DIALOG_HEIGHT" value="890"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/> <item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/>
</section> </section>

View File

@@ -1,5 +1,5 @@
INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core
690321491.index 1256436118.index
1872440599.index 1872440599.index
2240786275.index 2240786275.index
4150628576.index 4150628576.index
@@ -8,13 +8,14 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
2982788279.index 2982788279.index
2626965509.index 2626965509.index
2609856074.index 2609856074.index
2769879155.index
4134502745.index 4134502745.index
2817101718.index 2817101718.index
4158338144.index 4158338144.index
519552992.index 519552992.index
2503368578.index 2503368578.index
2181028596.index 2181028596.index
9341915.index
900586112.index
1453089870.index 1453089870.index
2593736024.index 2593736024.index
815902026.index 815902026.index
@@ -23,6 +24,7 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
96642630.index 96642630.index
2488355463.index 2488355463.index
1446719945.index 1446719945.index
2721734530.index
1118739196.index 1118739196.index
2891161224.index 2891161224.index
2047888269.index 2047888269.index
@@ -43,11 +45,11 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
1732769785.index 1732769785.index
2838468603.index 2838468603.index
1436262503.index 1436262503.index
3662169204.index 2668411497.index
2927822381.index 2927822381.index
2398089967.index 2398089967.index
225562445.index 225562445.index
2668411497.index 3662169204.index
1295630681.index 1295630681.index
3135354350.index 3135354350.index
3602551868.index 3602551868.index
@@ -100,6 +102,6 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
198314732.index 198314732.index
3514612140.index 3514612140.index
1324521365.index 1324521365.index
2318770678.index
1633924572.index 1633924572.index
1256436118.index 2318770678.index
690321491.index

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<dirs> <dirs>
<entry loc="/snap/eclipse/131/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.9.v20251105-0741/jre" stamp="1765305526000"/> <entry loc="/usr/lib/jvm/java-21-openjdk-amd64" stamp="1773694107630"/>
</dirs> </dirs>

View File

@@ -12,6 +12,7 @@
<entry path="null"/> <entry path="null"/>
</endorsedDirs> </endorsedDirs>
</libraryInfo> </libraryInfo>
<libraryInfo home="/usr/lib/jvm/java-21-openjdk-amd64" version="21.0.10"/>
<libraryInfo home="/snap/eclipse/131/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.9.v20251105-0741" version="21.0.9"> <libraryInfo home="/snap/eclipse/131/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.9.v20251105-0741" version="21.0.9">
<bootpath> <bootpath>
<entry path="null"/> <entry path="null"/>
@@ -23,5 +24,4 @@
<entry path="null"/> <entry path="null"/>
</endorsedDirs> </endorsedDirs>
</libraryInfo> </libraryInfo>
<libraryInfo home="/snap/eclipse/131/usr/lib/eclipse/plugins/org.eclipse.justj.openjdk.hotspot.jre.full.linux.x86_64_21.0.9.v20251105-0741/jre" version="21.0.9"/>
</libraryInfos> </libraryInfos>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<typeInfoHistroy> <typeInfoHistroy>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{AboController.java[AboController" modifiers="1" timestamp="1773400404000"/> <typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{AboController.java[AboController" modifiers="1" timestamp="1773400404000"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.cardlock{CardLockService.java[CardLockService" modifiers="1" timestamp="1773773086076"/>
</typeInfoHistroy> </typeInfoHistroy>

View File

@@ -7,11 +7,15 @@
<fullyQualifiedTypeName name="org.hibernate.grammars.hql.HqlParser.LocalDateTimeContext"/> <fullyQualifiedTypeName name="org.hibernate.grammars.hql.HqlParser.LocalDateTimeContext"/>
<fullyQualifiedTypeName name="java.util.stream.Collectors"/> <fullyQualifiedTypeName name="java.util.stream.Collectors"/>
<fullyQualifiedTypeName name="java.time.LocalDate"/> <fullyQualifiedTypeName name="java.time.LocalDate"/>
<fullyQualifiedTypeName name="java.time.LocalDateTime"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.CardDTO"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.CardDTO"/>
<fullyQualifiedTypeName name="java.lang.Enum"/> <fullyQualifiedTypeName name="java.lang.Enum"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.GreenCard"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.GreenCard"/>
<fullyQualifiedTypeName name="java.util.Random"/> <fullyQualifiedTypeName name="java.util.Random"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.CardLockService"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.CardLockService"/>
<fullyQualifiedTypeName name="jakarta.persistence.Column"/> <fullyQualifiedTypeName name="jakarta.persistence.Column"/>
<fullyQualifiedTypeName name="java.time.LocalDateTime"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.history.LockHistoryRepository"/>
<fullyQualifiedTypeName name="lombok.Getter"/>
<fullyQualifiedTypeName name="lombok.Setter"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.Test"/>
</qualifiedTypeNameHistroy> </qualifiedTypeNameHistroy>

View File

@@ -6,7 +6,7 @@
<item key="layout" value="1"/> <item key="layout" value="1"/>
<item key="rootMode" value="1"/> <item key="rootMode" value="1"/>
<item key="linkWithEditor" value="true"/> <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;1&quot; rootMode=&quot;1&quot; workingSetName=&quot;Aggregate for window 1773600542055&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.m2e.MavenModuleFilter&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.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 1773600542055&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>
@@ -58,4 +58,14 @@
<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_SIZE" value="true"/>
<item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_USE_PERSISTED_LOCATION" value="false"/> <item key="org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControlDIALOG_USE_PERSISTED_LOCATION" value="false"/>
</section> </section>
<section name="NewPackageCreationWizard.dialogBounds">
<item key="DIALOG_X_ORIGIN" value="20"/>
<item key="DIALOG_Y_ORIGIN" value="20"/>
<item key="DIALOG_WIDTH" value="613"/>
<item key="DIALOG_HEIGHT" value="500"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/>
</section>
<section name="BuildPathsPropertyPage">
<item key="pageIndex" value="3"/>
</section>
</section> </section>

View File

@@ -17,3 +17,6 @@
2026-03-16 08:15:37,891 [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-16 08:15:37,891 [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-16 19:25:54,797 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update. 2026-03-16 19:25:54,797 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
2026-03-16 20:07:45,547 [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-16 20:07:45,547 [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-17 07:10:24,915 [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-17 19:39:55,931 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
2026-03-17 19:49:51,508 [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.

View File

@@ -1,3 +1,3 @@
#Mon Mar 16 20:07:41 CET 2026 #Tue Mar 17 19:49:49 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

View File

@@ -18,6 +18,10 @@ repositories {
} }
dependencies { dependencies {
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testCompileOnly("org.projectlombok:lombok")
testAnnotationProcessor("org.projectlombok:lombok")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")

View File

@@ -2,8 +2,10 @@ package de.oaa.xxx;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableScheduling
public class XxxThegameApplication { public class XxxThegameApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -1,5 +1,6 @@
package de.oaa.xxx.config; package de.oaa.xxx.config;
import jakarta.servlet.DispatcherType;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -30,6 +31,7 @@ public class SecurityConfig {
.authenticationEntryPoint((request, response, authException) -> .authenticationEntryPoint((request, response, authException) ->
response.sendRedirect("/login.html"))) response.sendRedirect("/login.html")))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.ERROR).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/userhome.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/userhome.html")).authenticated()
@@ -43,7 +45,8 @@ public class SecurityConfig {
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionvanilla.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionvanilla.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsm.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsm.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionchastity.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionchastity.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionchastityingame.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/neulock.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/activelock.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtasks.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtasks.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtoys.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmtoys.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmingame.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/sessionbdsmingame.html")).authenticated()
@@ -57,15 +60,20 @@ public class SecurityConfig {
.requestMatchers(AntPathRequestMatcher.antMatcher("/communityvotes.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/communityvotes.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/keyholder.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/keyholder.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/meine-locks.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/meine-locks.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/unlock-history.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/einladungen.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/einladungen.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/joinlock.html")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/joinlock.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/benachrichtigungen.html")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/gruppen/**")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/gruppen/**")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/feed/**")).authenticated() .requestMatchers(AntPathRequestMatcher.antMatcher("/feed/**")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/notifications/**")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/events/**")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.html")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/*.html")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/css/**")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/css/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/js/**")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/js/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/images/**")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/images/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/favicon.ico")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/favicon.ico")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/audio/**")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.png")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/*.png")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.jpg")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/*.jpg")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.svg")).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/*.svg")).permitAll()

View File

@@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity; import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository; import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@@ -34,6 +35,7 @@ public class LockeeInvitationController {
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final MessageRepository messageRepository; private final MessageRepository messageRepository;
private final SseService sseService;
@Value("${app.base-url:http://localhost:8080}") @Value("${app.base-url:http://localhost:8080}")
private String baseUrl; private String baseUrl;
@@ -43,11 +45,27 @@ public class LockeeInvitationController {
public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository, public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository,
CardlockRepository cardlockRepository, CardlockRepository cardlockRepository,
UserRepository userRepository, UserRepository userRepository,
MessageRepository messageRepository) { MessageRepository messageRepository,
SseService sseService) {
this.lockeeInvitationRepository = lockeeInvitationRepository; this.lockeeInvitationRepository = lockeeInvitationRepository;
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.messageRepository = messageRepository; this.messageRepository = messageRepository;
this.sseService = sseService;
}
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) {
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", java.util.Map.of("unreadCount", unread, "text", text));
} }
private String generateUnlockCode(int lines) { private String generateUnlockCode(int lines) {
@@ -134,13 +152,9 @@ public class LockeeInvitationController {
var lock = lockOpt.get(); var lock = lockOpt.get();
cardlockRepository.delete(lock); cardlockRepository.delete(lock);
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity(); sendMessage(myId, inv.getLockeeUserId(),
msg.setMessageId(UUID.randomUUID()); me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
msg.setSenderId(myId); null);
msg.setReceiverId(inv.getLockeeUserId());
msg.setText(me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.");
msg.setSentAt(LocalDateTime.now());
messageRepository.save(msg);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -232,14 +246,9 @@ public class LockeeInvitationController {
lockeeInvitationRepository.delete(inv); lockeeInvitationRepository.delete(inv);
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity(); sendMessage(myId, inv.getKeyholderUserId(),
msg.setMessageId(UUID.randomUUID()); me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
msg.setSenderId(myId); "/keyholder.html");
msg.setReceiverId(inv.getKeyholderUserId());
msg.setText(me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.\n\n" +
"Deine Keyholder-Seite: " + baseUrl + "/keyholder.html");
msg.setSentAt(now);
messageRepository.save(msg);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(), "lockId", lock.getLockId().toString(),
@@ -267,13 +276,9 @@ public class LockeeInvitationController {
var lock = lockOpt.get(); var lock = lockOpt.get();
cardlockRepository.delete(lock); cardlockRepository.delete(lock);
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity(); sendMessage(myId, inv.getKeyholderUserId(),
msg.setMessageId(UUID.randomUUID()); me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",
msg.setSenderId(myId); null);
msg.setReceiverId(inv.getKeyholderUserId());
msg.setText(me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.");
msg.setSentAt(LocalDateTime.now());
messageRepository.save(msg);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();

View File

@@ -1,7 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
public interface Card { public interface Card {
public CardDTO processCard(CardLockService lock); public CardDTO processCard(CardLockService lock);

View File

@@ -1,27 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
import de.oaa.xxx.games.chastity.KeyholderInvitationEntity;
import de.oaa.xxx.games.chastity.KeyholderInvitationRepository;
import de.oaa.xxx.games.chastity.LockeeInvitationEntity;
import de.oaa.xxx.games.chastity.LockeeInvitationRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity;
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
import de.oaa.xxx.games.chastity.CodeCreator;
import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.user.UserRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@@ -42,6 +20,39 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import de.oaa.xxx.games.chastity.CodeCreator;
import de.oaa.xxx.games.chastity.KeyholderInvitationEntity;
import de.oaa.xxx.games.chastity.KeyholderInvitationRepository;
import de.oaa.xxx.games.chastity.LockeeInvitationEntity;
import de.oaa.xxx.games.chastity.LockeeInvitationRepository;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity;
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import de.oaa.xxx.user.UserRepository;
@RestController @RestController
@RequestMapping("/keyholder") @RequestMapping("/keyholder")
public class CardLockController { public class CardLockController {
@@ -55,6 +66,11 @@ public class CardLockController {
private final HygieneViolationRepository hygieneViolationRepository; private final HygieneViolationRepository hygieneViolationRepository;
private final MessageRepository messageRepository; private final MessageRepository messageRepository;
private final LockeeInvitationRepository lockeeInvitationRepository; private final LockeeInvitationRepository lockeeInvitationRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final UnlockCodeHistoryRepository unlockCodeHistoryRepository;
private final SseService sseService;
@Value("${app.base-url:http://localhost:8080}") @Value("${app.base-url:http://localhost:8080}")
private String baseUrl; private String baseUrl;
@@ -67,7 +83,12 @@ public class CardLockController {
VerificationVoteRepository verificationVoteRepository, VerificationVoteRepository verificationVoteRepository,
HygieneViolationRepository hygieneViolationRepository, HygieneViolationRepository hygieneViolationRepository,
MessageRepository messageRepository, MessageRepository messageRepository,
LockeeInvitationRepository lockeeInvitationRepository) { LockeeInvitationRepository lockeeInvitationRepository,
AssignedTaskRepository assignedTaskRepository,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository,
UnlockCodeHistoryRepository unlockCodeHistoryRepository,
SseService sseService) {
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.cardLockRepository = cardLockRepository; this.cardLockRepository = cardLockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
@@ -77,6 +98,11 @@ public class CardLockController {
this.hygieneViolationRepository = hygieneViolationRepository; this.hygieneViolationRepository = hygieneViolationRepository;
this.messageRepository = messageRepository; this.messageRepository = messageRepository;
this.lockeeInvitationRepository = lockeeInvitationRepository; this.lockeeInvitationRepository = lockeeInvitationRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.unlockCodeHistoryRepository = unlockCodeHistoryRepository;
this.sseService = sseService;
} }
record CreateCardLockRequest( record CreateCardLockRequest(
@@ -94,7 +120,8 @@ public class CardLockController {
List<Task> tasks, List<Task> tasks,
boolean requiresVerification, boolean requiresVerification,
boolean testLock, boolean testLock,
Integer unlockCodeLines Integer unlockCodeLines,
String taskCardMode
) {} ) {}
private static final SecureRandom RNG = new SecureRandom(); private static final SecureRandom RNG = new SecureRandom();
@@ -143,6 +170,7 @@ public class CardLockController {
lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
lock.setRequiresVerification(req.requiresVerification()); lock.setRequiresVerification(req.requiresVerification());
lock.setTestLock(false); lock.setTestLock(false);
lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM");
// startTime, unlockCode, unlockCodeLines left null until lockee accepts // startTime, unlockCode, unlockCodeLines left null until lockee accepts
cardlockRepository.save(lock); cardlockRepository.save(lock);
@@ -157,14 +185,9 @@ public class CardLockController {
lockeeInvitationRepository.save(inv); lockeeInvitationRepository.save(inv);
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
MessageEntity msg = new MessageEntity(); sendMessage(myId, lockee.getUserId(),
msg.setMessageId(UUID.randomUUID()); me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
msg.setSenderId(myId); "/einladungen.html");
msg.setReceiverId(lockee.getUserId());
msg.setText(me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.\n\n" +
"Deine Einladungen findest du hier: " + baseUrl + "/einladungen.html");
msg.setSentAt(now);
messageRepository.save(msg);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"lockId", lock.getLockId().toString(), "lockId", lock.getLockId().toString(),
@@ -194,6 +217,7 @@ public class CardLockController {
lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
lock.setRequiresVerification(req.requiresVerification()); lock.setRequiresVerification(req.requiresVerification());
lock.setTestLock(req.testLock()); lock.setTestLock(req.testLock());
lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM");
lock.setUnlockCodeLines(codeLines); lock.setUnlockCodeLines(codeLines);
lock.setUnlockCode(unlockCode); lock.setUnlockCode(unlockCode);
@@ -224,15 +248,9 @@ public class CardLockController {
invitationRepository.save(inv); invitationRepository.save(inv);
String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock"; String lockName = req.name() != null && !req.name().isBlank() ? req.name() : "Unbenanntes Lock";
sendMessage(me.getUserId(), kh.getUserId(),
MessageEntity msg = new MessageEntity(); me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
msg.setMessageId(UUID.randomUUID()); "/einladungen.html");
msg.setSenderId(me.getUserId());
msg.setReceiverId(kh.getUserId());
msg.setText(me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.\n\n" +
"Deine Einladungen findest du hier: " + baseUrl + "/einladungen.html");
msg.setSentAt(now);
messageRepository.save(msg);
keyholderPending = true; keyholderPending = true;
} }
@@ -261,9 +279,45 @@ public class CardLockController {
CardDTO dto = service.getNextCard(); CardDTO dto = service.getNextCard();
if (dto == null) return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar")); if (dto == null) return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar"));
// Task-Karte in nicht-zufälligem Modus → Entscheidung delegieren
String taskPending = null;
if (dto.card() == CardEnum.TASK && !"RANDOM".equals(l.getTaskCardMode())
&& l.getTasks() != null && !l.getTasks().isEmpty()) {
if ("KEYHOLDER".equals(l.getTaskCardMode()) && l.getKeyholder() != null) {
KeyholderTaskChoiceEntity choice = new KeyholderTaskChoiceEntity();
choice.setLockId(l.getLockId());
choice.setCreatedAt(LocalDateTime.now());
choice.setStatus("PENDING");
keyholderTaskChoiceRepository.save(choice);
userRepository.findById(l.getKeyholder()).ifPresent(kh ->
sendMessage(l.getLockee(), kh.getUserId(),
"Deine Lockee hat eine Aufgaben-Karte gezogen wähle eine Aufgabe aus.",
"/keyholder.html"));
taskPending = "KEYHOLDER";
} else if ("COMMUNITY".equals(l.getTaskCardMode())) {
CommunityTaskVoteEntity vote = new CommunityTaskVoteEntity();
vote.setLockId(l.getLockId());
vote.setCreatedAt(LocalDateTime.now());
vote.setExpiresAt(LocalDateTime.now().plusHours(1));
vote.setStatus("ACTIVE");
vote.setTestLock(l.isTestLock());
communityTaskVoteRepository.save(vote);
taskPending = l.isTestLock() ? "RANDOM" : "COMMUNITY";
}
}
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("card", dto.card().name()); result.put("card", dto.card().name());
result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : ""); result.put("unlockCode", dto.unlockCode() != null ? dto.unlockCode() : "");
if (taskPending != null) result.put("taskPending", taskPending);
// Grüne Karte → Entsperrcode-Historie speichern
if (dto.unlockCode() != null && !dto.unlockCode().isBlank()) {
saveUnlockCodeHistory(myId, l.getLockId(), l.getName(), dto.unlockCode(), "GREEN_CARD");
}
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} }
@@ -282,6 +336,8 @@ public class CardLockController {
l.setHygineOpeningtime(LocalDateTime.now()); l.setHygineOpeningtime(LocalDateTime.now());
cardlockRepository.save(l); cardlockRepository.save(l);
saveUnlockCodeHistory(myId, l.getLockId(), l.getName(), l.getUnlockCode(), "HYGIENE_OPEN");
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"unlockCode", l.getUnlockCode(), "unlockCode", l.getUnlockCode(),
"durationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 30 "durationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 30
@@ -309,7 +365,11 @@ public class CardLockController {
long overtimeMinutes = ChronoUnit.MINUTES.between(dueTime, now); long overtimeMinutes = ChronoUnit.MINUTES.between(dueTime, now);
if (l.getKeyholder() == null) { if (l.getKeyholder() == null) {
// Self-Lock: 4-fache Überschreitungszeit einfrieren // Self-Lock: 4-fache Überschreitungszeit einfrieren
if (l.getFrozenUntill() != null) {
l.setFrozenUntill(l.getFrozenUntill().plusMinutes(overtimeMinutes * 4));
} else {
l.setFrozenUntill(now.plusMinutes(overtimeMinutes * 4)); l.setFrozenUntill(now.plusMinutes(overtimeMinutes * 4));
}
} else { } else {
// Keyholder vorhanden: Verletzung protokollieren // Keyholder vorhanden: Verletzung protokollieren
HygieneViolationEntity violation = new HygieneViolationEntity(); HygieneViolationEntity violation = new HygieneViolationEntity();
@@ -333,6 +393,8 @@ public class CardLockController {
l.setUnlockCode(newCode); l.setUnlockCode(newCode);
cardlockRepository.save(l); cardlockRepository.save(l);
saveUnlockCodeHistory(myId, l.getLockId(), l.getName(), newCode, "HYGIENE_CLOSE");
return ResponseEntity.ok(Map.of("newUnlockCode", newCode)); return ResponseEntity.ok(Map.of("newUnlockCode", newCode));
} }
@@ -424,6 +486,8 @@ public class CardLockController {
result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : ""); result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : "");
result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null); result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null);
result.put("currentTask", l.getCurrentTask() != null ? l.getCurrentTask() : null); result.put("currentTask", l.getCurrentTask() != null ? l.getCurrentTask() : null);
result.put("currentTaskDescription", l.getCurrentTaskDescription());
result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null);
result.put("hygieneEnabled", hygieneEnabled); result.put("hygieneEnabled", hygieneEnabled);
result.put("hygieneOpeningDue", hygieneOpeningDue); result.put("hygieneOpeningDue", hygieneOpeningDue);
result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
@@ -475,6 +539,79 @@ public class CardLockController {
result.put("verificationPendingId", verificationPendingId); result.put("verificationPendingId", verificationPendingId);
result.put("verificationPendingCode", verificationPendingCode); result.put("verificationPendingCode", verificationPendingCode);
// Abgelaufene Aufgaben prüfen und Strafe anwenden
boolean lockDirty = false;
var expiredTasks = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING")
.stream().filter(t -> t.getAcceptDeadline().isBefore(LocalDateTime.now())).toList();
for (var t : expiredTasks) {
t.setStatus("EXPIRED");
applyAssignedTaskPenalty(l, t);
assignedTaskRepository.save(t);
lockDirty = true;
sendMessage(l.getKeyholder(), l.getLockee(),
"Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.",
"/activelock.html?lockId=" + l.getLockId());
}
if (lockDirty) cardlockRepository.save(l);
// Ausstehende Keyholder-Aufgaben (ohne Aufgabentext)
var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING")
.stream()
.filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now()))
.map(t -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("taskId", t.getTaskId().toString());
m.put("taskTitle", t.getTaskTitle() != null ? t.getTaskTitle() : t.getTaskText());
m.put("taskDescription", t.getTaskDescription() != null ? t.getTaskDescription() : "");
m.put("taskMinutes", t.getTaskMinutes() != null ? t.getTaskMinutes() : 0);
m.put("assignedAt", t.getAssignedAt().toString());
m.put("acceptDeadline", t.getAcceptDeadline().toString());
m.put("penaltyFreezeMinutes", t.getPenaltyFreezeMinutes() != null ? t.getPenaltyFreezeMinutes() : 0);
m.put("penaltyRedCards", t.getPenaltyRedCards() != null ? t.getPenaltyRedCards() : 0);
return m;
})
.toList();
result.put("assignedTasks", pendingAssigned);
result.put("taskCardMode", l.getTaskCardMode());
// Ausstehende Keyholder-Choices
boolean pendingKeyholderChoice = !keyholderTaskChoiceRepository
.findByLockIdAndStatus(l.getLockId(), "PENDING").isEmpty();
result.put("pendingKeyholderChoice", pendingKeyholderChoice);
// Aktive Community-Vote
var activeVotes = communityTaskVoteRepository.findByStatus("ACTIVE").stream()
.filter(v -> v.getLockId().equals(l.getLockId())).findFirst();
if (activeVotes.isPresent()) {
var v = activeVotes.get();
result.put("activeCommunityVote", Map.of(
"voteSessionId", v.getVoteSessionId().toString(),
"expiresAt", v.getExpiresAt().toString()
));
}
// Notfall-Entsperrung: nach 1 Stunde automatisch öffnen
if (l.getEmergencyUnlockRequestedAt() != null
&& !l.isKeyholderRequestedUnlock()
&& l.getEmergencyUnlockRequestedAt().isBefore(LocalDateTime.now().minusHours(1))) {
l.setEmergencyAutoUnlocked(true);
l.setKeyholderRequestedUnlock(true);
cardlockRepository.save(l);
}
// Keyholder hat Unlock angefordert → Unlock-Code mitliefern
result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
if (l.isKeyholderRequestedUnlock()) {
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
saveUnlockCodeHistory(myId, l.getLockId(), l.getName(), l.getUnlockCode(), "KEYHOLDER_UNLOCK");
}
result.put("testLock", l.isTestLock());
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
if (l.isTestLock()) {
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
}
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} }
@@ -643,6 +780,7 @@ public class CardLockController {
msg.setReceiverId(lock.getLockee()); msg.setReceiverId(lock.getLockee());
msg.setText(me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt."); msg.setText(me.getName() + " hat die Einladung als Keyholder*In für das Lock „" + lockName + "\" abgelehnt.");
msg.setSentAt(LocalDateTime.now()); msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg); messageRepository.save(msg);
} }
@@ -704,6 +842,7 @@ public class CardLockController {
msg.setReceiverId(inv.getKeyholderUserId()); msg.setReceiverId(inv.getKeyholderUserId());
msg.setText(me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen."); msg.setText(me.getName() + " hat die Keyholder-Einladung für das Lock „" + lockName + "\" zurückgezogen.");
msg.setSentAt(LocalDateTime.now()); msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg); messageRepository.save(msg);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -729,6 +868,10 @@ public class CardLockController {
item.put("lockeeProfilePic", lockee.getProfilePicture()); item.put("lockeeProfilePic", lockee.getProfilePicture());
item.put("totalCards", lock.getAvailableCards() != null ? lock.getAvailableCards().size() : 0); item.put("totalCards", lock.getAvailableCards() != null ? lock.getAvailableCards().size() : 0);
item.put("startTime", lock.getStartTime() != null ? lock.getStartTime().toString() : null); item.put("startTime", lock.getStartTime() != null ? lock.getStartTime().toString() : null);
boolean frozenByKh = lock.getFrozenUntill() != null
&& lock.getFrozenUntill().isAfter(LocalDateTime.now())
&& (lock.getCurrentTask() == null || lock.getCurrentTask().isBlank());
item.put("isFrozenByKeyholder", frozenByKh);
result.add(item); result.add(item);
} }
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
@@ -811,7 +954,11 @@ public class CardLockController {
result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0); result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0);
result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : null); result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : null);
result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null); result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null);
result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null);
boolean isFrozenByKeyholder = l.getFrozenUntill() != null && l.getFrozenUntill().isAfter(LocalDateTime.now());
result.put("isFrozenByKeyholder", isFrozenByKeyholder);
result.put("currentTask", l.getCurrentTask()); result.put("currentTask", l.getCurrentTask());
result.put("currentTaskDescription", l.getCurrentTaskDescription());
result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null); result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null);
result.put("hygieneEnabled", hygieneEnabled); result.put("hygieneEnabled", hygieneEnabled);
result.put("hygieneOpeningDue", hygieneOpeningDue); result.put("hygieneOpeningDue", hygieneOpeningDue);
@@ -827,6 +974,66 @@ public class CardLockController {
result.put("verificationDownvotes", verificationDownvotes); result.put("verificationDownvotes", verificationDownvotes);
result.put("hygieneViolations", recentViolations); result.put("hygieneViolations", recentViolations);
result.put("hasTasks", l.getTasks() != null && !l.getTasks().isEmpty()); result.put("hasTasks", l.getTasks() != null && !l.getTasks().isEmpty());
if (l.getTasks() != null) {
var taskList = l.getTasks().stream()
.map(t -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("title", t.resolveTitle());
m.put("description", t.getDescription() != null ? t.getDescription() : "");
m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
return m;
})
.toList();
result.put("taskList", taskList);
} else {
result.put("taskList", List.of());
}
var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING")
.stream()
.filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now()))
.map(t -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("taskId", t.getTaskId().toString());
m.put("taskTitle", t.getTaskTitle() != null ? t.getTaskTitle() : t.getTaskText());
m.put("taskDescription", t.getTaskDescription() != null ? t.getTaskDescription() : "");
m.put("taskMinutes", t.getTaskMinutes() != null ? t.getTaskMinutes() : 0);
m.put("assignedAt", t.getAssignedAt().toString());
m.put("acceptDeadline", t.getAcceptDeadline().toString());
m.put("penaltyFreezeMinutes", t.getPenaltyFreezeMinutes() != null ? t.getPenaltyFreezeMinutes() : 0);
m.put("penaltyRedCards", t.getPenaltyRedCards() != null ? t.getPenaltyRedCards() : 0);
return m;
})
.toList();
result.put("pendingAssignedTasks", pendingAssigned);
result.put("taskCardMode", l.getTaskCardMode());
// Ausstehende Task-Karten-Choices (KEYHOLDER-Modus)
List<Task> lockTasks = l.getTasks() != null ? l.getTasks() : List.of();
List<Map<String, Object>> taskListForChoice = new ArrayList<>();
for (int i = 0; i < lockTasks.size(); i++) {
Task t = lockTasks.get(i);
Map<String, Object> tm = new LinkedHashMap<>();
tm.put("index", i);
tm.put("title", t.resolveTitle());
tm.put("description", t.getDescription() != null ? t.getDescription() : "");
tm.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
taskListForChoice.add(tm);
}
var pendingChoices = keyholderTaskChoiceRepository.findByLockIdAndStatus(lockId, "PENDING")
.stream()
.map(c -> {
Map<String, Object> cm = new LinkedHashMap<>();
cm.put("choiceId", c.getChoiceId().toString());
cm.put("createdAt", c.getCreatedAt().toString());
cm.put("tasks", taskListForChoice);
return cm;
})
.toList();
result.put("pendingTaskChoices", pendingChoices);
result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
result.put("emergencyUnlockRequestedAt", l.getEmergencyUnlockRequestedAt() != null ? l.getEmergencyUnlockRequestedAt().toString() : null);
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
} }
@@ -899,6 +1106,7 @@ public class CardLockController {
msg.setReceiverId(l.getLockee()); msg.setReceiverId(l.getLockee());
msg.setText(msgText); msg.setText(msgText);
msg.setSentAt(LocalDateTime.now()); msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg); messageRepository.save(msg);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -963,11 +1171,383 @@ public class CardLockController {
msg.setReceiverId(l.getLockee()); msg.setReceiverId(l.getLockee());
msg.setText(msgText); msg.setText(msgText);
msg.setSentAt(LocalDateTime.now()); msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
messageRepository.save(msg); messageRepository.save(msg);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
// ── Hilfsmethoden ──────────────────────────────────────────────────────────
private void applyAssignedTaskPenalty(CardLockEntity l, AssignedTaskEntity task) {
if (task.getPenaltyFreezeMinutes() != null && task.getPenaltyFreezeMinutes() > 0) {
LocalDateTime until = LocalDateTime.now().plusMinutes(task.getPenaltyFreezeMinutes());
// Bestehenden Freeze nur verlängern, nie verkürzen
if (l.getFrozenUntill() == null || until.isAfter(l.getFrozenUntill())) {
l.setFrozenUntill(until);
l.setNextCardIn(until);
}
}
if (task.getPenaltyRedCards() != null && task.getPenaltyRedCards() > 0) {
List<CardEnum> cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of());
for (int i = 0; i < task.getPenaltyRedCards(); i++) {
cards.add(CardEnum.RED);
}
l.setAvailableCards(cards);
}
}
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl) {
if (senderId == null || receiverId == null) return;
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(senderId);
msg.setReceiverId(receiverId);
msg.setText(text);
msg.setSentAt(LocalDateTime.now());
msg.setSystemMessage(true);
if (targetUrl != null) msg.setTargetUrl(targetUrl);
messageRepository.save(msg);
long unread = messageRepository.countByReceiverIdAndSystemMessageAndReadAtIsNull(receiverId, true);
sseService.push(receiverId, "NOTIFICATION", java.util.Map.of("unreadCount", unread, "text", text));
}
// ── Entsperrcode-Historie ──────────────────────────────────────────────────
private void saveUnlockCodeHistory(UUID userId, UUID lockId, String lockName, String unlockCode, String source) {
if (unlockCode == null || unlockCode.isBlank()) return;
// Deduplizierung: gleicher Code+Quelle+Lock bereits gespeichert → überspringen
if (unlockCodeHistoryRepository.existsByLockIdAndSourceAndUnlockCode(lockId, source, unlockCode)) return;
UnlockCodeHistoryEntity entry = new UnlockCodeHistoryEntity();
entry.setUserId(userId);
entry.setLockId(lockId);
entry.setLockName(lockName != null && !lockName.isBlank() ? lockName : "Unbenanntes Lock");
entry.setUnlockCode(unlockCode);
entry.setSource(source);
entry.setReceivedAt(LocalDateTime.now());
unlockCodeHistoryRepository.save(entry);
// Nur die letzten 10 behalten
long count = unlockCodeHistoryRepository.countByUserId(userId);
if (count > 10) {
var oldest = unlockCodeHistoryRepository.findByUserIdOrderByReceivedAtAsc(userId);
for (int i = 0; i < count - 10; i++) {
unlockCodeHistoryRepository.delete(oldest.get(i));
}
}
}
@GetMapping("/cardlock/unlock-history")
public ResponseEntity<List<Map<String, Object>>> getUnlockHistory(Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var entries = unlockCodeHistoryRepository.findByUserIdOrderByReceivedAtDesc(myId,
org.springframework.data.domain.PageRequest.of(0, 10));
List<Map<String, Object>> result = new ArrayList<>();
for (var e : entries) {
Map<String, Object> item = new HashMap<>();
item.put("lockName", e.getLockName());
item.put("unlockCode", e.getUnlockCode());
item.put("source", e.getSource());
item.put("receivedAt", e.getReceivedAt().toString());
result.add(item);
}
return ResponseEntity.ok(result);
}
// ── Keyholder: Aufgabe stellen ─────────────────────────────────────────────
record AssignTaskRequest(int taskIndex, int acceptDeadlineMinutes,
Integer penaltyFreezeMinutes, Integer penaltyRedCards) {}
@Transactional
@PostMapping("/as-keyholder/{lockId}/assign-task")
public ResponseEntity<?> assignTask(@PathVariable UUID lockId,
@RequestBody AssignTaskRequest req,
Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var me = meOpt.get();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!me.getUserId().equals(l.getKeyholder())) return ResponseEntity.status(403).build();
var tasks = l.getTasks();
if (tasks == null || tasks.isEmpty())
return ResponseEntity.badRequest().body(Map.of("error", "Dieses Lock hat keine Aufgaben."));
if (req.taskIndex() < 0 || req.taskIndex() >= tasks.size())
return ResponseEntity.badRequest().body(Map.of("error", "Ungültiger Aufgaben-Index."));
if (req.acceptDeadlineMinutes() < 1)
return ResponseEntity.badRequest().body(Map.of("error", "Die Annahme-Frist muss mindestens 1 Minute betragen."));
long pendingCount = assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING").stream()
.filter(t -> t.getAcceptDeadline().isAfter(LocalDateTime.now()))
.count();
if (pendingCount >= 5)
return ResponseEntity.badRequest().body(Map.of("error", "Es sind bereits 5 Aufgaben offen. Bitte warte, bis der Lockee eine davon annimmt oder ablehnt."));
Task task = tasks.get(req.taskIndex());
AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lockId);
assigned.setTaskTitle(task.resolveTitle());
assigned.setTaskDescription(task.getDescription());
assigned.setTaskText(task.resolveTitle()); // Compat
assigned.setTaskMinutes(task.getMinutes());
assigned.setAssignedAt(LocalDateTime.now());
assigned.setAcceptDeadline(LocalDateTime.now().plusMinutes(req.acceptDeadlineMinutes()));
assigned.setPenaltyFreezeMinutes(req.penaltyFreezeMinutes());
assigned.setPenaltyRedCards(req.penaltyRedCards());
assigned.setStatus("PENDING");
assignedTaskRepository.save(assigned);
sendMessage(me.getUserId(), l.getLockee(),
me.getName() + " hat dir eine Aufgabe gestellt. Du hast " +
req.acceptDeadlineMinutes() + " Minuten, um sie anzunehmen.",
"/activelock.html?lockId=" + lockId);
return ResponseEntity.noContent().build();
}
// ── Lockee: Aufgabe annehmen ───────────────────────────────────────────────
@Transactional
@PostMapping("/cardlock/{lockId}/assigned-tasks/{taskId}/accept")
public ResponseEntity<?> acceptAssignedTask(@PathVariable UUID lockId,
@PathVariable UUID taskId,
Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
var taskOpt = assignedTaskRepository.findById(taskId);
if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId))
return ResponseEntity.notFound().build();
var task = taskOpt.get();
if (!"PENDING".equals(task.getStatus()))
return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend."));
if (task.getAcceptDeadline().isBefore(LocalDateTime.now())) {
// Bereits abgelaufen Strafe anwenden
task.setStatus("EXPIRED");
applyAssignedTaskPenalty(l, task);
assignedTaskRepository.save(task);
cardlockRepository.save(l);
return ResponseEntity.status(409).body(Map.of("error", "Die Annahme-Frist ist abgelaufen. Die Strafe wurde angewendet."));
}
boolean hasActiveTask = (l.getCurrentTask() != null && !l.getCurrentTask().isBlank())
|| (l.getTaskFrozenUntil() != null && l.getTaskFrozenUntil().isAfter(LocalDateTime.now()));
if (hasActiveTask)
return ResponseEntity.status(409).body(Map.of("error", "Du hast bereits eine laufende Aufgabe."));
// Aufgabe aktivieren separater Task-Timer, kein Freeze
String title = task.getTaskTitle() != null ? task.getTaskTitle() : task.getTaskText();
l.setCurrentTask(title);
l.setCurrentTaskDescription(task.getTaskDescription());
if (task.getTaskMinutes() != null && task.getTaskMinutes() > 0) {
l.setTaskFrozenUntil(LocalDateTime.now().plusMinutes(task.getTaskMinutes()));
// Fälligkeit aller anderen offenen Aufgaben um die Task-Dauer verschieben
final int extraMinutes = task.getTaskMinutes();
assignedTaskRepository.findByLockIdAndStatus(lockId, "PENDING").stream()
.filter(t -> !t.getTaskId().equals(taskId))
.forEach(t -> {
t.setAcceptDeadline(t.getAcceptDeadline().plusMinutes(extraMinutes));
assignedTaskRepository.save(t);
});
}
task.setStatus("ACCEPTED");
assignedTaskRepository.save(task);
cardlockRepository.save(l);
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe angenommen.", "/keyholder.html");
return ResponseEntity.noContent().build();
}
// ── Lockee: Aufgabe ablehnen ───────────────────────────────────────────────
@Transactional
@PostMapping("/cardlock/{lockId}/assigned-tasks/{taskId}/decline")
public ResponseEntity<?> declineAssignedTask(@PathVariable UUID lockId,
@PathVariable UUID taskId,
Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
var taskOpt = assignedTaskRepository.findById(taskId);
if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId))
return ResponseEntity.notFound().build();
var task = taskOpt.get();
if (!"PENDING".equals(task.getStatus()))
return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend."));
task.setStatus("DECLINED");
applyAssignedTaskPenalty(l, task);
assignedTaskRepository.save(task);
cardlockRepository.save(l);
sendMessage(myId, l.getKeyholder(), meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", "/keyholder.html");
return ResponseEntity.noContent().build();
}
// ── Keyholder: Aufgabe zurückziehen ───────────────────────────────────────
@Transactional
@DeleteMapping("/as-keyholder/{lockId}/assigned-tasks/{taskId}")
public ResponseEntity<?> cancelAssignedTask(@PathVariable UUID lockId,
@PathVariable UUID taskId,
Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();
var taskOpt = assignedTaskRepository.findById(taskId);
if (taskOpt.isEmpty() || !taskOpt.get().getLockId().equals(lockId))
return ResponseEntity.notFound().build();
var task = taskOpt.get();
if (!"PENDING".equals(task.getStatus()))
return ResponseEntity.status(409).body(Map.of("error", "Aufgabe ist nicht mehr ausstehend."));
assignedTaskRepository.delete(task);
return ResponseEntity.noContent().build();
}
record FreezeRequest(String frozenUntil) {}
@Transactional
@PostMapping("/as-keyholder/{lockId}/freeze")
public ResponseEntity<?> freezeLock(@PathVariable UUID lockId,
@RequestBody FreezeRequest req,
Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var me = meOpt.get();
UUID myId = me.getUserId();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();
if (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "Das Lock ist gerade durch eine Aufgabe eingefroren."));
}
LocalDateTime until;
try {
until = LocalDateTime.parse(req.frozenUntil());
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat."));
}
if (!until.isAfter(LocalDateTime.now())) {
return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen."));
}
l.setFrozenUntill(until);
cardlockRepository.save(l);
sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock bis " +
until.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) +
" " + until.toLocalTime().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) +
" Uhr eingefroren.",
"/activelock.html?lockId=" + lockId);
return ResponseEntity.noContent().build();
}
@Transactional
@DeleteMapping("/as-keyholder/{lockId}/freeze")
public ResponseEntity<?> unfreezeLock(@PathVariable UUID lockId, Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var me = meOpt.get();
UUID myId = me.getUserId();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();
if (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "Das Lock ist durch eine Aufgabe eingefroren und kann nicht manuell entfroren werden."));
}
l.setFrozenUntill(null);
cardlockRepository.save(l);
sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.",
"/activelock.html?lockId=" + lockId);
return ResponseEntity.noContent().build();
}
@Transactional
@PostMapping("/as-keyholder/{lockId}/request-unlock")
public ResponseEntity<?> requestUnlock(@PathVariable UUID lockId, Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();
l.setKeyholderRequestedUnlock(true);
cardlockRepository.save(l);
sendMessage(myId, l.getLockee(),
"Dein Keyholder hat das Lock freigeschaltet. Du erhältst beim nächsten Laden deinen Entsperrcode.",
"/activelock.html?lockId=" + lockId);
return ResponseEntity.noContent().build();
}
@Transactional
@PostMapping("/cardlock/{lockId}/emergency-unlock")
public ResponseEntity<?> requestEmergencyUnlock(@PathVariable UUID lockId, Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
var me = meOpt.get();
UUID myId = me.getUserId();
var lockOpt = cardlockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
if (l.isTestLock()) return ResponseEntity.badRequest().build();
if (l.getEmergencyUnlockRequestedAt() != null) return ResponseEntity.status(409).build();
l.setEmergencyUnlockRequestedAt(LocalDateTime.now());
if (l.getKeyholder() == null) {
// Self-Lock ohne Keyholderin → sofort öffnen
l.setEmergencyAutoUnlocked(true);
l.setKeyholderRequestedUnlock(true);
} else {
// Keyholderin benachrichtigen
sendMessage(myId, l.getKeyholder(),
"⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde, sonst öffnet sich das Lock automatisch.",
"/keyholder.html");
}
cardlockRepository.save(l);
return ResponseEntity.noContent().build();
}
private String cardLabel(CardEnum card) { private String cardLabel(CardEnum card) {
return switch (card) { return switch (card) {
case RED -> "Rote Karte"; case RED -> "Rote Karte";

View File

@@ -76,12 +76,32 @@ public class CardLockEntity {
private LocalDateTime unlockTime; private LocalDateTime unlockTime;
@Column @Column
private String currentTask; private String currentTask;
@Column(columnDefinition = "TEXT")
private String currentTaskDescription;
@Column
private LocalDateTime taskFrozenUntil;
@Convert(converter = TaskListConverter.class) @Convert(converter = TaskListConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<Task> tasksInQueue; private List<Task> tasksInQueue;
@Column @Column
private String unlockCode; private String unlockCode;
/** Keyholder hat Unlock angefordert nächste Aktion der Lockee zeigt grüne Karte */
@Column(nullable = false)
private boolean keyholderRequestedUnlock = false;
/** Lockee hat Notfall-Entsperrung angefordert */
@Column
private java.time.LocalDateTime emergencyUnlockRequestedAt;
/** true = System hat automatisch entsperrt (Keyholderin nicht reagiert) */
@Column(nullable = false)
private boolean emergencyAutoUnlocked = false;
/** RANDOM | KEYHOLDER | COMMUNITY */
@Column(nullable = false)
private String taskCardMode = "RANDOM";
public UUID getLockId() { public UUID getLockId() {
return lockId; return lockId;
} }
@@ -242,13 +262,14 @@ public class CardLockEntity {
this.tasks = tasks; this.tasks = tasks;
} }
public String getCurrentTask() { public String getCurrentTask() { return currentTask; }
return currentTask; public void setCurrentTask(String currentTask) { this.currentTask = currentTask; }
}
public void setCurrentTask(String currentTask) { public String getCurrentTaskDescription() { return currentTaskDescription; }
this.currentTask = currentTask; public void setCurrentTaskDescription(String d) { this.currentTaskDescription = d; }
}
public LocalDateTime getTaskFrozenUntil() { return taskFrozenUntil; }
public void setTaskFrozenUntil(LocalDateTime t) { this.taskFrozenUntil = t; }
public List<Task> getTasksInQueue() { public List<Task> getTasksInQueue() {
return tasksInQueue; return tasksInQueue;
@@ -289,4 +310,16 @@ public class CardLockEntity {
public void setUnlockCodeLines(Integer unlockCodeLines) { public void setUnlockCodeLines(Integer unlockCodeLines) {
this.unlockCodeLines = unlockCodeLines; this.unlockCodeLines = unlockCodeLines;
} }
public String getTaskCardMode() { return taskCardMode != null ? taskCardMode : "RANDOM"; }
public void setTaskCardMode(String taskCardMode) { this.taskCardMode = taskCardMode; }
public boolean isKeyholderRequestedUnlock() { return keyholderRequestedUnlock; }
public void setKeyholderRequestedUnlock(boolean v) { this.keyholderRequestedUnlock = v; }
public java.time.LocalDateTime getEmergencyUnlockRequestedAt() { return emergencyUnlockRequestedAt; }
public void setEmergencyUnlockRequestedAt(java.time.LocalDateTime t) { this.emergencyUnlockRequestedAt = t; }
public boolean isEmergencyAutoUnlocked() { return emergencyAutoUnlocked; }
public void setEmergencyAutoUnlocked(boolean v) { this.emergencyAutoUnlocked = v; }
} }

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.games.chastity; package de.oaa.xxx.games.chastity.cardlock;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
@@ -11,10 +11,7 @@ import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import de.oaa.xxx.games.chastity.cardlock.CardDTO; import de.oaa.xxx.games.chastity.ProcessLock;
import de.oaa.xxx.games.chastity.cardlock.CardEnum;
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
import de.oaa.xxx.games.chastity.cardlock.CardLockRepository;
import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.verification.VerificationEntity; import de.oaa.xxx.games.chastity.verification.VerificationEntity;
import de.oaa.xxx.games.chastity.verification.VerificationRepository; import de.oaa.xxx.games.chastity.verification.VerificationRepository;
@@ -39,7 +36,9 @@ public class CardLockService extends ProcessLock {
public CardDTO getNextCard() { public CardDTO getNextCard() {
LOGGER.debug("New Card requested by user {}", lock.getLockee()); LOGGER.debug("New Card requested by user {}", lock.getLockee());
CardDTO card = null; CardDTO card = null;
if (lock.isAccumulatePicks()) { if (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now())) {
card = getGreenCard();
} else if (lock.isAccumulatePicks()) {
if (lock.getNextCardIn().isAfter(LocalDateTime.now())) { if (lock.getNextCardIn().isAfter(LocalDateTime.now())) {
lock.setOpenPicks(lock.getOpenPicks() == null ? 1 : lock.getOpenPicks() + 1); lock.setOpenPicks(lock.getOpenPicks() == null ? 1 : lock.getOpenPicks() + 1);
} }
@@ -51,7 +50,6 @@ public class CardLockService extends ProcessLock {
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) { if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
lock.setNextCardIn(LocalDateTime.now().plusMinutes(lock.getPickEveryMinute())); lock.setNextCardIn(LocalDateTime.now().plusMinutes(lock.getPickEveryMinute()));
card = getRandomCard(); card = getRandomCard();
lock.getAvailableCards().remove(card.card());
} }
} }
cardLockRepository.save(lock); cardLockRepository.save(lock);
@@ -67,6 +65,10 @@ public class CardLockService extends ProcessLock {
return card.get().processCard(this); return card.get().processCard(this);
} }
LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte"); LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte");
return getGreenCard();
}
private CardDTO getGreenCard() {
return new CardDTO(CardEnum.GREEN, lock.getUnlockCode()); return new CardDTO(CardEnum.GREEN, lock.getUnlockCode());
} }
@@ -91,7 +93,8 @@ public class CardLockService extends ProcessLock {
public void unlock(String unlockCode) { public void unlock(String unlockCode) {
this.lock.setUnlockTime(LocalDateTime.now()); this.lock.setUnlockTime(LocalDateTime.now());
boolean valid = true; // Self-Lock oder automatische Entsperrung ohne Keyholder-Zustimmung ungültig
boolean valid = lock.getKeyholder() != null && !lock.isEmergencyAutoUnlocked();
if (!this.lock.isTestLock()) { if (!this.lock.isTestLock()) {
if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) { if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream() Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
@@ -159,6 +162,11 @@ public class CardLockService extends ProcessLock {
} }
public String task() { public String task() {
// Non-RANDOM modes are handled by the controller after the card is drawn
if (!"RANDOM".equals(lock.getTaskCardMode())) {
LOGGER.debug("Task card drawn in {} mode skipping random assignment", lock.getTaskCardMode());
return "";
}
LOGGER.debug("Apply random task"); LOGGER.debug("Apply random task");
var tasks = lock.getTasks(); var tasks = lock.getTasks();
if (!tasks.isEmpty()) { if (!tasks.isEmpty()) {
@@ -169,17 +177,19 @@ public class CardLockService extends ProcessLock {
public String task(Task task) { public String task(Task task) {
LOGGER.debug("Apply task {}", task); LOGGER.debug("Apply task {}", task);
lock.setCurrentTask(task.getText()); lock.setCurrentTask(task.resolveTitle());
if (task.getMinutes() != null) { lock.setCurrentTaskDescription(task.getDescription());
freeze(task.getMinutes()); if (task.getMinutes() != null && task.getMinutes() > 0) {
lock.setTaskFrozenUntil(LocalDateTime.now().plusMinutes(task.getMinutes()));
} }
return ""; return "";
} }
public String clearTask() { public String clearTask() {
LOGGER.debug("Clear task"); LOGGER.debug("Clear task");
lock.setFrozenUntill(null);
lock.setCurrentTask(null); lock.setCurrentTask(null);
lock.setCurrentTaskDescription(null);
lock.setTaskFrozenUntil(null);
return ""; return "";
} }

View File

@@ -32,7 +32,8 @@ public class CardlockTemplateController {
Integer hygineOpeningDurationMinutes, Integer hygineOpeningDurationMinutes,
Integer hygineOpeningEveryMinites, Integer hygineOpeningEveryMinites,
List<Task> tasks, List<Task> tasks,
boolean requiresVerification boolean requiresVerification,
String taskCardMode
) {} ) {}
private Map<String, Object> toDto(CardlockTemplateEntity t) { private Map<String, Object> toDto(CardlockTemplateEntity t) {
@@ -48,6 +49,7 @@ public class CardlockTemplateController {
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes()); dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of()); dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
dto.put("requiresVerification", t.isRequiresVerification()); dto.put("requiresVerification", t.isRequiresVerification());
dto.put("taskCardMode", t.getTaskCardMode());
return dto; return dto;
} }
@@ -127,5 +129,6 @@ public class CardlockTemplateController {
t.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); t.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes());
t.setTasks(req.tasks() != null ? req.tasks() : List.of()); t.setTasks(req.tasks() != null ? req.tasks() : List.of());
t.setRequiresVerification(req.requiresVerification()); t.setRequiresVerification(req.requiresVerification());
t.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM");
} }
} }

View File

@@ -53,6 +53,9 @@ public class CardlockTemplateEntity {
@Column @Column
private boolean requiresVerification; private boolean requiresVerification;
@Column(nullable = false)
private String taskCardMode = "RANDOM";
public UUID getTemplateId() { return templateId; } public UUID getTemplateId() { return templateId; }
public void setTemplateId(UUID templateId) { this.templateId = templateId; } public void setTemplateId(UUID templateId) { this.templateId = templateId; }
@@ -88,4 +91,7 @@ public class CardlockTemplateEntity {
public boolean isRequiresVerification() { return requiresVerification; } public boolean isRequiresVerification() { return requiresVerification; }
public void setRequiresVerification(boolean requiresVerification) { this.requiresVerification = requiresVerification; } public void setRequiresVerification(boolean requiresVerification) { this.requiresVerification = requiresVerification; }
public String getTaskCardMode() { return taskCardMode != null ? taskCardMode : "RANDOM"; }
public void setTaskCardMode(String taskCardMode) { this.taskCardMode = taskCardMode; }
} }

View File

@@ -0,0 +1,56 @@
package de.oaa.xxx.games.chastity.cardlock;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "community_task_vote")
public class CommunityTaskVoteEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID voteSessionId;
@Column(nullable = false)
private UUID lockId;
/** ACTIVE | COMPLETED */
@Column(nullable = false)
private String status = "ACTIVE";
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime expiresAt;
/** true = TestLock, nicht der Community zeigen */
@Column(nullable = false)
private boolean testLock = false;
/** null until completed */
@Column
private Integer winningTaskIndex;
public UUID getVoteSessionId() { return voteSessionId; }
public void setVoteSessionId(UUID id) { this.voteSessionId = id; }
public UUID getLockId() { return lockId; }
public void setLockId(UUID lockId) { this.lockId = lockId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime t) { this.createdAt = t; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public void setExpiresAt(LocalDateTime t) { this.expiresAt = t; }
public boolean isTestLock() { return testLock; }
public void setTestLock(boolean testLock) { this.testLock = testLock; }
public Integer getWinningTaskIndex() { return winningTaskIndex; }
public void setWinningTaskIndex(Integer i) { this.winningTaskIndex = i; }
}

View File

@@ -0,0 +1,35 @@
package de.oaa.xxx.games.chastity.cardlock;
import jakarta.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "community_task_vote_entry",
uniqueConstraints = @UniqueConstraint(columnNames = {"voteSessionId", "voterUserId"}))
public class CommunityTaskVoteEntryEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID entryId;
@Column(nullable = false)
private UUID voteSessionId;
@Column(nullable = false)
private UUID voterUserId;
@Column(nullable = false)
private int taskIndex;
public UUID getEntryId() { return entryId; }
public void setEntryId(UUID entryId) { this.entryId = entryId; }
public UUID getVoteSessionId() { return voteSessionId; }
public void setVoteSessionId(UUID id) { this.voteSessionId = id; }
public UUID getVoterUserId() { return voterUserId; }
public void setVoterUserId(UUID id) { this.voterUserId = id; }
public int getTaskIndex() { return taskIndex; }
public void setTaskIndex(int taskIndex) { this.taskIndex = taskIndex; }
}

View File

@@ -0,0 +1,11 @@
package de.oaa.xxx.games.chastity.cardlock;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface CommunityTaskVoteEntryRepository extends JpaRepository<CommunityTaskVoteEntryEntity, UUID> {
List<CommunityTaskVoteEntryEntity> findByVoteSessionId(UUID voteSessionId);
boolean existsByVoteSessionIdAndVoterUserId(UUID voteSessionId, UUID voterUserId);
Integer countByVoteSessionIdAndTaskIndex(UUID voteSessionId, int taskIndex);
}

View File

@@ -0,0 +1,12 @@
package de.oaa.xxx.games.chastity.cardlock;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public interface CommunityTaskVoteRepository extends JpaRepository<CommunityTaskVoteEntity, UUID> {
List<CommunityTaskVoteEntity> findByStatus(String status);
List<CommunityTaskVoteEntity> findByStatusAndExpiresAtBefore(String status, LocalDateTime time);
boolean existsByLockIdAndStatus(UUID lockId, String status);
}

View File

@@ -1,7 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
public class DoubleUpCard implements Card { public class DoubleUpCard implements Card {
@Override @Override

View File

@@ -1,7 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
public class FreezeCard implements Card { public class FreezeCard implements Card {
@Override @Override

View File

@@ -1,7 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
public class GreenCard implements Card { public class GreenCard implements Card {
@Override @Override

View File

@@ -0,0 +1,36 @@
package de.oaa.xxx.games.chastity.cardlock;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "keyholder_task_choice")
public class KeyholderTaskChoiceEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID choiceId;
@Column(nullable = false)
private UUID lockId;
/** PENDING | CHOSEN */
@Column(nullable = false)
private String status = "PENDING";
@Column(nullable = false)
private LocalDateTime createdAt;
public UUID getChoiceId() { return choiceId; }
public void setChoiceId(UUID choiceId) { this.choiceId = choiceId; }
public UUID getLockId() { return lockId; }
public void setLockId(UUID lockId) { this.lockId = lockId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime t) { this.createdAt = t; }
}

View File

@@ -0,0 +1,9 @@
package de.oaa.xxx.games.chastity.cardlock;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface KeyholderTaskChoiceRepository extends JpaRepository<KeyholderTaskChoiceEntity, UUID> {
List<KeyholderTaskChoiceEntity> findByLockIdAndStatus(UUID lockId, String status);
}

View File

@@ -1,7 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
public class RedCard implements Card { public class RedCard implements Card {
@Override @Override

View File

@@ -1,7 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
public class ResetCard implements Card { public class ResetCard implements Card {
@Override @Override

View File

@@ -1,7 +1,5 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.CardLockService;
public class TaskCard implements Card { public class TaskCard implements Card {
@Override @Override

View File

@@ -0,0 +1,249 @@
package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.*;
@RestController
@RequestMapping("/task-card")
public class TaskCardController {
private final CardlockRepository cardlockRepository;
private final UserRepository userRepository;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
public TaskCardController(CardlockRepository cardlockRepository,
UserRepository userRepository,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository,
SseService sseService) {
this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
}
// ── Keyholder: ausstehende Aufgaben-Karten-Entscheidungen ─────────────────
@GetMapping("/keyholder/choices")
@Transactional(readOnly = true)
public ResponseEntity<List<Map<String, Object>>> getPendingChoices(Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
// Alle Locks bei denen ich Keyholder bin
var locks = cardlockRepository.findByKeyholderAndUnlockTimeIsNull(myId);
List<Map<String, Object>> result = new ArrayList<>();
for (var lock : locks) {
var pending = keyholderTaskChoiceRepository.findByLockIdAndStatus(lock.getLockId(), "PENDING");
if (pending.isEmpty()) continue;
var lockee = userRepository.findById(lock.getLockee()).orElse(null);
List<Map<String, Object>> taskList = buildTaskList(lock.getTasks());
for (var choice : pending) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("choiceId", choice.getChoiceId().toString());
m.put("lockId", lock.getLockId().toString());
m.put("lockName", lock.getName() != null ? lock.getName() : "");
m.put("lockeeName", lockee != null ? lockee.getName() : "");
m.put("createdAt", choice.getCreatedAt().toString());
m.put("tasks", taskList);
result.add(m);
}
}
return ResponseEntity.ok(result);
}
record PenaltyRequest(Integer penaltyFreezeMinutes, Integer penaltyRedCards) {}
@PostMapping("/keyholder/choices/{choiceId}/choose/{taskIndex}")
@Transactional
public ResponseEntity<Void> chooseTask(@PathVariable UUID choiceId,
@PathVariable int taskIndex,
@org.springframework.web.bind.annotation.RequestBody(required = false) PenaltyRequest penalty,
Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var choiceOpt = keyholderTaskChoiceRepository.findById(choiceId);
if (choiceOpt.isEmpty()) return ResponseEntity.notFound().build();
var choice = choiceOpt.get();
var lockOpt = cardlockRepository.findById(choice.getLockId());
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var lock = lockOpt.get();
if (!myId.equals(lock.getKeyholder())) return ResponseEntity.status(403).build();
if (!"PENDING".equals(choice.getStatus())) return ResponseEntity.status(409).build();
List<Task> tasks = lock.getTasks();
if (tasks == null || taskIndex < 0 || taskIndex >= tasks.size())
return ResponseEntity.badRequest().build();
Task task = tasks.get(taskIndex);
AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lock.getLockId());
assigned.setTaskTitle(task.resolveTitle());
assigned.setTaskDescription(task.getDescription());
assigned.setTaskText(task.resolveTitle());
assigned.setTaskMinutes(task.getMinutes());
assigned.setAssignedAt(LocalDateTime.now());
assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1));
assigned.setStatus("PENDING");
if (penalty != null) {
assigned.setPenaltyFreezeMinutes(penalty.penaltyFreezeMinutes());
assigned.setPenaltyRedCards(penalty.penaltyRedCards());
}
assignedTaskRepository.save(assigned);
choice.setStatus("CHOSEN");
keyholderTaskChoiceRepository.save(choice);
sendMessage(myId, lock.getLockee(),
"Dein Keyholder hat eine Aufgabe für dich ausgewählt.",
"/activelock.html?lockId=" + lock.getLockId());
return ResponseEntity.noContent().build();
}
// ── Community: aktive Abstimmungen ────────────────────────────────────────
@GetMapping("/community/votes")
@Transactional(readOnly = true)
public ResponseEntity<List<Map<String, Object>>> getActiveVotes(Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var activeVotes = communityTaskVoteRepository.findByStatus("ACTIVE");
List<Map<String, Object>> result = new ArrayList<>();
for (var vote : activeVotes) {
if (vote.isTestLock()) continue;
var lockOpt = cardlockRepository.findById(vote.getLockId());
if (lockOpt.isEmpty()) continue;
var lock = lockOpt.get();
var lockee = userRepository.findById(lock.getLockee()).orElse(null);
List<Task> tasks = lock.getTasks();
if (tasks == null || tasks.isEmpty()) continue;
List<Map<String, Object>> taskList = buildTaskList(tasks);
// Stimmenanzahl pro Task
List<Integer> voteCounts = new ArrayList<>();
for (int i = 0; i < tasks.size(); i++) {
voteCounts.add(communityTaskVoteEntryRepository
.countByVoteSessionIdAndTaskIndex(vote.getVoteSessionId(), i));
}
Integer myVote = communityTaskVoteEntryRepository.findByVoteSessionId(vote.getVoteSessionId())
.stream().filter(e -> myId.equals(e.getVoterUserId()))
.map(CommunityTaskVoteEntryEntity::getTaskIndex).findFirst().orElse(null);
Map<String, Object> m = new LinkedHashMap<>();
m.put("voteSessionId", vote.getVoteSessionId().toString());
m.put("lockId", lock.getLockId().toString());
m.put("lockeeName", lockee != null ? lockee.getName() : "");
m.put("expiresAt", vote.getExpiresAt().toString());
m.put("tasks", taskList);
m.put("voteCounts", voteCounts);
m.put("myVote", myVote);
result.add(m);
}
return ResponseEntity.ok(result);
}
@PostMapping("/community/votes/{voteSessionId}/vote/{taskIndex}")
@Transactional
public ResponseEntity<Void> castVote(@PathVariable UUID voteSessionId,
@PathVariable int taskIndex,
Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var voteOpt = communityTaskVoteRepository.findById(voteSessionId);
if (voteOpt.isEmpty()) return ResponseEntity.notFound().build();
var vote = voteOpt.get();
if (!"ACTIVE".equals(vote.getStatus()) || vote.getExpiresAt().isBefore(LocalDateTime.now()))
return ResponseEntity.status(409).build();
var lockOpt = cardlockRepository.findById(vote.getLockId());
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var lock = lockOpt.get();
if (lock.getTasks() == null || taskIndex < 0 || taskIndex >= lock.getTasks().size())
return ResponseEntity.badRequest().build();
if (communityTaskVoteEntryRepository.existsByVoteSessionIdAndVoterUserId(voteSessionId, myId))
return ResponseEntity.status(409).build();
CommunityTaskVoteEntryEntity entry = new CommunityTaskVoteEntryEntity();
entry.setVoteSessionId(voteSessionId);
entry.setVoterUserId(myId);
entry.setTaskIndex(taskIndex);
communityTaskVoteEntryRepository.save(entry);
return ResponseEntity.noContent().build();
}
// ── Helpers ───────────────────────────────────────────────────────────────
private List<Map<String, Object>> buildTaskList(List<Task> tasks) {
if (tasks == null) return List.of();
List<Map<String, Object>> list = new ArrayList<>();
for (int i = 0; i < tasks.size(); i++) {
Task t = tasks.get(i);
Map<String, Object> m = new LinkedHashMap<>();
m.put("index", i);
m.put("title", t.resolveTitle());
m.put("description", t.getDescription() != null ? t.getDescription() : "");
m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
list.add(m);
}
return list;
}
private void sendMessage(UUID fromId, UUID toId, String text, String targetUrl) {
if (toId == null) return;
MessageEntity msg = new MessageEntity();
msg.setMessageId(java.util.UUID.randomUUID());
msg.setSenderId(fromId);
msg.setReceiverId(toId);
msg.setText(text);
msg.setSystemMessage(true);
msg.setTargetUrl(targetUrl);
msg.setSentAt(java.time.LocalDateTime.now());
messageRepository.save(msg);
sseService.push(toId, "notification", Map.of("text", text));
}
}

View File

@@ -0,0 +1,132 @@
package de.oaa.xxx.games.chastity.cardlock;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.social.SseService;
import de.oaa.xxx.social.entity.MessageEntity;
import de.oaa.xxx.social.repository.MessageRepository;
@Component
public class TaskVoteScheduler {
private static final Logger LOG = LoggerFactory.getLogger(TaskVoteScheduler.class);
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final CardlockRepository cardlockRepository;
private final AssignedTaskRepository assignedTaskRepository;
private final MessageRepository messageRepository;
private final SseService sseService;
public TaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
CardlockRepository cardlockRepository,
AssignedTaskRepository assignedTaskRepository,
MessageRepository messageRepository,
SseService sseService) {
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.cardlockRepository = cardlockRepository;
this.assignedTaskRepository = assignedTaskRepository;
this.messageRepository = messageRepository;
this.sseService = sseService;
}
@Scheduled(fixedDelay = 60_000)
@Transactional
public void processExpiredVotes() {
var expired = communityTaskVoteRepository
.findByStatusAndExpiresAtBefore("ACTIVE", LocalDateTime.now());
for (var vote : expired) {
LOG.debug("Processing expired community task vote {}", vote.getVoteSessionId());
var lockOpt = cardlockRepository.findById(vote.getLockId());
if (lockOpt.isEmpty()) {
vote.setStatus("COMPLETED");
communityTaskVoteRepository.save(vote);
continue;
}
var lock = lockOpt.get();
List<Task> tasks = lock.getTasks();
if (tasks == null || tasks.isEmpty()) {
vote.setStatus("COMPLETED");
communityTaskVoteRepository.save(vote);
continue;
}
// Stimmen auszählen
var entries = communityTaskVoteEntryRepository.findByVoteSessionId(vote.getVoteSessionId());
int winnerIndex;
if (entries.isEmpty()) {
winnerIndex = new Random().nextInt(tasks.size());
LOG.debug("No votes → random task index {}", winnerIndex);
} else {
int[] counts = new int[tasks.size()];
for (var e : entries) {
if (e.getTaskIndex() >= 0 && e.getTaskIndex() < tasks.size()) {
counts[e.getTaskIndex()]++;
}
}
int max = Arrays.stream(counts).max().getAsInt();
// Alle Tasks mit Maximalstimmen sammeln → zufällige Auswahl bei Gleichstand
List<Integer> winners = new ArrayList<>();
for (int i = 0; i < counts.length; i++) {
if (counts[i] == max) winners.add(i);
}
winnerIndex = winners.get(new Random().nextInt(winners.size()));
LOG.debug("Vote winner: task index {} with {} votes", winnerIndex, max);
}
Task task = tasks.get(winnerIndex);
AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lock.getLockId());
assigned.setTaskTitle(task.resolveTitle());
assigned.setTaskDescription(task.getDescription());
assigned.setTaskText(task.resolveTitle());
assigned.setTaskMinutes(task.getMinutes());
assigned.setAssignedAt(LocalDateTime.now());
assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1));
assigned.setStatus("PENDING");
assignedTaskRepository.save(assigned);
vote.setStatus("COMPLETED");
vote.setWinningTaskIndex(winnerIndex);
communityTaskVoteRepository.save(vote);
// Lockee benachrichtigen
sendMessage(lock.getLockee(),
"Die Community hat für deine Aufgabe abgestimmt: \"" + task.resolveTitle() + "\"",
"/activelock.html?lockId=" + lock.getLockId());
}
}
private void sendMessage(UUID toId, String text, String targetUrl) {
if (toId == null) return;
MessageEntity msg = new MessageEntity();
msg.setMessageId(UUID.randomUUID());
msg.setSenderId(toId); // System-Nachricht, kein echter Sender
msg.setReceiverId(toId);
msg.setText(text);
msg.setSystemMessage(true);
msg.setTargetUrl(targetUrl);
msg.setSentAt(LocalDateTime.now());
messageRepository.save(msg);
sseService.push(toId, "notification", Map.of("text", text));
}
}

View File

@@ -0,0 +1,47 @@
package de.oaa.xxx.games.chastity.cardlock;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "unlock_code_history")
public class UnlockCodeHistoryEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false)
private UUID userId;
@Column(nullable = false)
private UUID lockId;
@Column
private String lockName;
@Column(nullable = false)
private String unlockCode;
/** GREEN_CARD | HYGIENE_OPEN | HYGIENE_CLOSE | KEYHOLDER_UNLOCK */
@Column(nullable = false)
private String source;
@Column(nullable = false)
private LocalDateTime receivedAt;
public UUID getId() { return id; }
public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; }
public UUID getLockId() { return lockId; }
public void setLockId(UUID lockId) { this.lockId = lockId; }
public String getLockName() { return lockName; }
public void setLockName(String lockName) { this.lockName = lockName; }
public String getUnlockCode() { return unlockCode; }
public void setUnlockCode(String unlockCode) { this.unlockCode = unlockCode; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public LocalDateTime getReceivedAt() { return receivedAt; }
public void setReceivedAt(LocalDateTime receivedAt) { this.receivedAt = receivedAt; }
}

View File

@@ -0,0 +1,23 @@
package de.oaa.xxx.games.chastity.cardlock;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.UUID;
public interface UnlockCodeHistoryRepository extends JpaRepository<UnlockCodeHistoryEntity, UUID> {
List<UnlockCodeHistoryEntity> findByUserIdOrderByReceivedAtDesc(UUID userId, Pageable pageable);
/** Prüft, ob für dieses Lock und diese Quelle bereits der gleiche Code gespeichert ist. */
boolean existsByLockIdAndSourceAndUnlockCode(UUID lockId, String source, String unlockCode);
/** Alle Einträge des Users aufsteigend (für Cleanup: älteste löschen). */
@Query("SELECT e FROM UnlockCodeHistoryEntity e WHERE e.userId = :userId ORDER BY e.receivedAt ASC")
List<UnlockCodeHistoryEntity> findByUserIdOrderByReceivedAtAsc(@Param("userId") UUID userId);
long countByUserId(UUID userId);
}

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