Weiter am Timelock gearbeitet

This commit is contained in:
2026-03-25 23:54:47 +01:00
parent eb741daf4c
commit 03118d339a
16 changed files with 2417 additions and 2029 deletions

View File

@@ -40,7 +40,19 @@
"Bash(stat /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/java/de/oaa/xxx/games/chastity/ttlock/*)",
"Bash(git -C /home/mario/Workspaces/xxx-thegame diff HEAD xxxthegame/src/main/resources/static/neulock.html)",
"Bash(1:*)",
"Bash(python3:*)"
"Bash(python3:*)",
"Bash(head -8 echo \"---\" grep -n \"✎\\\\|edit\\\\|bearbeit\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/aufgaben.html)",
"Bash(head -8 echo \"---\" grep -n \"🚿\\\\|hygiene\\\\|hygien\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/activelock.html)",
"Bash(head -8 echo \"---\" grep -n \"🆘\\\\|SOS\\\\|nothilfe\\\\|emer\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/activelock.html)",
"Bash(head -8 echo \"---\" grep -n \"👍\\\\|👎\\\\|vote\\\\|abstimm\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/communityvotes.html)",
"Bash(head -8 echo \"---\" grep -n \"🧊\\\\|ice\\\\|eis\\\\|freeze\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/activetimelock.html)",
"Bash(head -8 echo \"---\" grep -n \"📁\\\\|folder\\\\|archiv\\\\|histor\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/activelock.html)",
"Bash(head -8 echo \"---\" grep -n \"🔄\\\\|refresh\\\\|reload\\\\|neu laden\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/activelock.html)",
"Bash(head -8 echo \"---\" grep -n \"🔥\\\\|fire\\\\|hot\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/nachrichten.html)",
"Bash(head -5 echo \"---\" grep -n \"⬆\\\\|⬇\\\\|sort\\\\|up\\\\|down\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/admin.html)",
"Bash(head -8 echo \"---\" grep -n \"⭐\\\\|star\\\\|premium\\\\|abo\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/abonnements.html)",
"Bash(head -8 echo \"---\" grep -n \"👁\\\\|view\\\\|sichtbar\\\\|Ansicht\" /home/mario/Workspaces/xxx-thegame/xxxthegame/src/main/resources/static/einladungen.html)",
"Bash(/dev/null echo:*)"
]
}
}

View File

@@ -1,5 +1,5 @@
#Wed Mar 25 07:26:10 CET 2026
#Wed Mar 25 21:32:43 CET 2026
display=\:0
host=mario-mint
process-id=4033
process-id=43084
user=mario

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -8,3 +8,4 @@
2026-03-24 06:41:47,661 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-24 11:26:24,107 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-25 07:26:14,133 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
2026-03-25 21:32:47,427 [Worker-8: 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 @@
#Wed Mar 25 07:26:10 CET 2026
#Wed Mar 25 21:32:43 CET 2026
org.eclipse.core.runtime=2
org.eclipse.platform=4.39.0.v20260226-0420

View File

@@ -1,13 +1,5 @@
Anzeige der Locks und Lockees im Profil, Anzeige der Locked time, falls verifiziert, die verification muss einmal am tag statt finden
Historie für die Spiele
Sammeln von Erfahrung
Verknüpfung der Spiele, wenn das Finish eines aus dem Bereich Chastity ist, wird daraus sofort ein Lock erstellt
Wenn ein Lock für einen User existiert, wird er ohne entsprechend Penis/Vagina zu dem Spiel erstellt.
TODO: Im Time Lock, wenn im Spinning Wheel tasks drin sind, dürfen keine sonst keine Tasks gefordert sein und umgekehrt
Ich kann Spieler einladen zu spielen, dann kriegt die Person eine E-Mail und muss bestätigen, dass es diese PErson ist, sie wird dann ins spiel übernommen

View File

@@ -28,7 +28,7 @@ import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "current_lock")
@Table(name = "active_lock")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "lock_type", discriminatorType = DiscriminatorType.STRING)
public class BaseLockEntity {

View File

@@ -42,6 +42,7 @@ import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity;
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository;
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository;
@@ -59,6 +60,7 @@ public class TimeLockController {
private final CommunityVerificationRepository verificationRepository;
private final CommunityVerificationVoteRepository verificationVoteRepository;
private final SubscriptionLimitService subscriptionLimitService;
private final UnlockCodeHistoryService unlockCodeHistoryService;
public TimeLockController(TimeLockRepository timeLockRepository,
TimeLockTemplateRepository templateRepository,
@@ -69,7 +71,8 @@ public class TimeLockController {
TimeLockServiceFactory timeLockServiceFactory,
CommunityVerificationRepository verificationRepository,
CommunityVerificationVoteRepository verificationVoteRepository,
SubscriptionLimitService subscriptionLimitService) {
SubscriptionLimitService subscriptionLimitService,
UnlockCodeHistoryService unlockCodeHistoryService) {
this.timeLockRepository = timeLockRepository;
this.templateRepository = templateRepository;
this.userRepository = userRepository;
@@ -80,6 +83,7 @@ public class TimeLockController {
this.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository;
this.subscriptionLimitService = subscriptionLimitService;
this.unlockCodeHistoryService = unlockCodeHistoryService;
}
// ── Erstellen ────────────────────────────────────────────────────────────────
@@ -357,7 +361,7 @@ public class TimeLockController {
}
result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
if (l.isKeyholderRequestedUnlock() || l.isTestLock()) {
if (l.isKeyholderRequestedUnlock() || l.isTestLock() || timeUp) {
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
}
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
@@ -584,6 +588,10 @@ public class TimeLockController {
}
TimeLockService service = timeLockServiceFactory.create(l);
if (l.getUnlockCode() != null && !l.getUnlockCode().isBlank()) {
String reason = l.isKeyholderRequestedUnlock() ? "KEYHOLDER_UNLOCK" : "LOCK_OPEN";
unlockCodeHistoryService.save(myId, lockId, l.getName(), l.getUnlockCode(), reason);
}
service.unlock(l.getUnlockCode());
// Clean up verifications
@@ -596,6 +604,123 @@ public class TimeLockController {
return ResponseEntity.noContent().build();
}
// ── Keyholder-Ansicht ─────────────────────────────────────────────────────────
@GetMapping("/timelock/as-keyholder")
public ResponseEntity<List<Map<String, Object>>> getTimeLocksAsKeyholder(Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName());
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
UUID myId = meOpt.get().getUserId();
var locks = timeLockRepository.findByKeyholderAndStartTimeIsNotNullAndUnlockTimeIsNull(myId);
List<Map<String, Object>> result = new ArrayList<>();
for (var lock : locks) {
var lockeeOpt = userRepository.findById(lock.getLockee());
if (lockeeOpt.isEmpty()) continue;
var lockee = lockeeOpt.get();
LocalDateTime now = LocalDateTime.now();
boolean isFrozen = lock.getFrozenFrom() != null
&& (lock.getFrozenUntil() == null || lock.getFrozenUntil().isAfter(now));
Map<String, Object> item = new LinkedHashMap<>();
item.put("lockId", lock.getLockId().toString());
item.put("lockType", "TIMELOCK");
item.put("lockName", lock.getName() != null ? lock.getName() : "TimeLock");
item.put("lockeeName", lockee.getName());
item.put("lockeeId", lockee.getUserId().toString());
item.put("lockeeProfilePic", lockee.getProfilePicture());
item.put("startTime", lock.getStartTime() != null ? lock.getStartTime().toString() : null);
item.put("isFrozen", isFrozen);
item.put("emergencyUnlockRequested", lock.getEmergencyUnlockRequestedAt() != null);
result.add(item);
}
return ResponseEntity.ok(result);
}
@GetMapping("/timelock/as-keyholder/{lockId}")
public ResponseEntity<Map<String, Object>> getTimeLockAsKeyholder(
@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 = timeLockRepository.findById(lockId);
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get();
if (!myId.equals(l.getKeyholder())) return ResponseEntity.status(403).build();
var lockeeOpt = userRepository.findById(l.getLockee());
if (lockeeOpt.isEmpty()) return ResponseEntity.notFound().build();
var lockee = lockeeOpt.get();
LocalDateTime now = LocalDateTime.now();
boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now);
boolean isFrozen = l.getFrozenFrom() != null
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
// Verifikation
boolean verificationDue = false;
boolean verificationDoneToday = false;
String verificationMyVote = null;
String verificationTodayId = null;
if (l.isRequiresVerification()) {
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1);
var completed = verificationRepository
.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId, todayStart, todayEnd);
if (!completed.isEmpty()) {
verificationDoneToday = true;
var todayV = completed.get(0);
verificationTodayId = todayV.getDisplayId().toString();
var myVote = verificationVoteRepository
.findAllByVerificationId(todayV.getDisplayId()).stream()
.filter(v -> myId.equals(v.getVoteId())).findFirst();
verificationMyVote = myVote.map(v -> v.isUpvote() ? "upvote" : "downvote").orElse(null);
} else {
verificationDue = true;
}
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("lockId", l.getLockId().toString());
result.put("lockType", "TIMELOCK");
result.put("lockName", l.getName() != null ? l.getName() : "TimeLock");
result.put("lockeeId", lockee.getUserId().toString());
result.put("lockeeName", lockee.getName());
result.put("lockeeProfilePic", lockee.getProfilePicture());
result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null);
result.put("unlockTime", (l.isEndTimeVisible() || timeUp)
? (l.getUnlockTime() != null ? l.getUnlockTime().toString() : null) : null);
result.put("timeUp", timeUp);
result.put("isFrozen", isFrozen);
result.put("frozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
result.put("requiresVerification", l.isRequiresVerification());
result.put("verificationDue", verificationDue);
result.put("verificationDoneToday", verificationDoneToday);
result.put("verificationMyVote", verificationMyVote);
result.put("verificationTodayId", verificationTodayId);
return ResponseEntity.ok(result);
}
@PostMapping("/timelock/as-keyholder/{lockId}/request-unlock")
@Transactional
public ResponseEntity<?> requestUnlockAsKeyholder(
@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 = timeLockRepository.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);
timeLockRepository.save(l);
return ResponseEntity.noContent().build();
}
// ── Notfall-Entsperrung ───────────────────────────────────────────────────────
@PostMapping("/timelock/{lockId}/emergency-unlock")
@@ -615,15 +740,21 @@ public class TimeLockController {
l.setEmergencyUnlockRequestedAt(LocalDateTime.now());
if (l.getKeyholder() == null) {
// Kein Keyholder: sofort entsperren, Lock als ungültig markieren (keine Historie, keine XP)
l.setEmergencyAutoUnlocked(true);
l.setKeyholderRequestedUnlock(true);
timeLockRepository.save(l);
if (l.getUnlockCode() != null && !l.getUnlockCode().isBlank()) {
unlockCodeHistoryService.save(myId, lockId, l.getName(), l.getUnlockCode(), "EMERGENCY_UNLOCK");
}
return ResponseEntity.ok(Map.of("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : ""));
} else {
systemMessageService.send(myId, l.getKeyholder(),
"⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde.",
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.EMERGENCY);
timeLockRepository.save(l);
return ResponseEntity.noContent().build();
}
timeLockRepository.save(l);
return ResponseEntity.noContent().build();
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────────

View File

@@ -1,5 +1,6 @@
package de.oaa.xxx.games.chastity.timelock;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -10,7 +11,9 @@ public interface TimeLockRepository extends JpaRepository<TimeLockEntity, UUID>
boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
@Modifying
List<TimeLockEntity> findByKeyholderAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID keyholder);
@Modifying(clearAutomatically = true)
@Query("DELETE FROM TimeLockEntity t WHERE t.lockId = :lockId")
void deleteByLockId(UUID lockId);
}

View File

@@ -1208,26 +1208,9 @@
async function lockLoeschen() {
closeWarnModal();
try {
const res = await fetch('/keyholder/timelock/' + lockId, { method: 'DELETE' });
if (res.ok) {
const data = await res.json().catch(() => ({}));
if (data.unlockCode) {
// Code kurz anzeigen bevor redirect
const area = document.getElementById('lockActionArea');
area.innerHTML = `
<div style="text-align:center;padding:1.5rem;background:rgba(46,204,113,0.08);border:1px solid rgba(46,204,113,0.3);border-radius:10px;">
<div style="font-size:0.85rem;color:var(--color-muted);margin-bottom:0.5rem;">Dein Entsperrcode:</div>
<div style="font-size:2rem;font-weight:700;font-family:monospace;letter-spacing:0.2em;color:#2ecc71;">${data.unlockCode}</div>
<div style="font-size:0.82rem;color:var(--color-muted);margin-top:0.5rem;">Lock erfolgreich beendet.</div>
</div>`;
setTimeout(() => { window.location.href = '/userhome.html'; }, 5000);
} else {
window.location.href = '/userhome.html';
}
}
} catch(_) {
window.location.href = '/userhome.html';
}
await fetch('/keyholder/timelock/' + lockId, { method: 'DELETE' });
} catch(_) { /* ignorieren */ }
window.location.href = '/userhome.html';
}
function openEmergencyModal() {
@@ -1253,13 +1236,30 @@
document.getElementById('emergencyModalActions').style.display = 'none';
try {
const res = await fetch('/keyholder/timelock/' + lockId + '/emergency-unlock', { method: 'POST' });
if (res.ok || res.status === 204) {
const hasKH2 = _currentLock && _currentLock.keyholderName;
const successText = hasKH2
? `✅ Notfall-Anfrage gesendet. ${_currentLock.keyholderName} wurde benachrichtigt.`
: `✅ Notfall-Freigabe ausgelöst. Das Lock öffnet sich jetzt.`;
if (res.status === 200) {
// Kein Keyholder: Entsperrcode direkt anzeigen, Lock als ungültig markiert
const data = await res.json().catch(() => ({}));
document.getElementById('emergencyModalContent').innerHTML = `
<p style="font-size:0.85rem;color:var(--color-muted);line-height:1.5;margin:0 0 0.75rem;">
⚠️ Das Lock wird als <strong style="color:var(--color-text);">ungültig</strong> gewertet
kein Historieneintrag, keine XP.
</p>
<div style="font-size:2rem;font-weight:700;font-family:monospace;letter-spacing:0.2em;
text-align:center;background:var(--color-secondary);border-radius:8px;
padding:0.75rem 1rem;color:var(--color-primary);margin-bottom:0.5rem;">
${data.unlockCode || ''}
</div>`;
const btn = document.getElementById('btnEmergencyConfirm');
btn.textContent = 'OK Lock beenden';
btn.onclick = async () => { closeEmergencyModal(); await lockLoeschen(); };
document.getElementById('emergencyModalActions').style.display = '';
} else if (res.status === 204) {
// Keyholder vorhanden: Benachrichtigung gesendet
const khName = _currentLock && _currentLock.keyholderName;
document.getElementById('emergencyModalContent').innerHTML =
`<p style="font-size:0.88rem;color:#2ecc71;line-height:1.5;margin:0;">${successText}</p>`;
`<p style="font-size:0.88rem;color:#2ecc71;line-height:1.5;margin:0;">
✅ Notfall-Anfrage gesendet. ${khName} wurde benachrichtigt.
</p>`;
setTimeout(() => { closeEmergencyModal(); loadLock(); }, 2000);
}
} catch(e) {

View File

@@ -24,6 +24,9 @@ window.ICONS = {
CHECK: { type: 'emoji', value: '✅' },
DISCOVER: { type: 'emoji', value: '🗺️' },
ARROW: { type: 'emoji', value: '▶️' },
REFRESH: { type: 'emoji', value: '🔄' }, // Erneuern / Neu laden
START: { type: 'emoji', value: '🚀' }, // Starten / Los
CELEBRATE: { type: 'emoji', value: '🎉' }, // Erfolg / Abschluss
// ── UI-Symbole ────────────────────────────────────────────────────────
CLOSE: { type: 'symbol', value: '✕' }, // Schließen / Ablehnen / Löschen
@@ -35,13 +38,47 @@ window.ICONS = {
LIGHTNING: { type: 'emoji', value: '⚡' }, // Aktion (z. B. Zeit entfernen)
EMOJI_PICKER: { type: 'emoji', value: '😊' }, // Emoji-Picker öffnen
REMOVE: { type: 'symbol', value: '⊗' }, // Eintrag/Spiel entfernen
EDIT: { type: 'symbol', value: '✎' }, // Bearbeiten-Button
TRASH: { type: 'emoji', value: '🗑' }, // Löschen-Button
WARNING: { type: 'emoji', value: '⚠️' }, // Warnung / Hinweis
REPORT: { type: 'symbol', value: '⚑' }, // Melden-Button (Flag)
VISIBILITY: { type: 'emoji', value: '👁' }, // Sichtbar / Details sichtbar
THUMBS_UP: { type: 'emoji', value: '👍' }, // Upvote / Zustimmung
THUMBS_DOWN: { type: 'emoji', value: '👎' }, // Downvote / Ablehnung
ARROW_UP: { type: 'symbol', value: '⬆' }, // Sortierung aufsteigend
ARROW_DOWN: { type: 'symbol', value: '⬇' }, // Sortierung absteigend
NAV_PREV: { type: 'symbol', value: '←' }, // Zurück / Vorheriges Bild
NAV_NEXT: { type: 'symbol', value: '→' }, // Weiter / Nächstes Bild
CAROUSEL_PREV: { type: 'symbol', value: '' }, // Karussell zurück
CAROUSEL_NEXT: { type: 'symbol', value: '' }, // Karussell weiter
TIP: { type: 'emoji', value: '💡' }, // Hinweis / Tipp
DOT_RED: { type: 'emoji', value: '🔴' }, // Status-Indikator rot
COMING_SOON: { type: 'emoji', value: '🚧' }, // In Entwicklung / Demnächst
// ── Chastity Game ─────────────────────────────────────────────────────
NEW_LOCK: { type: 'emoji', value: '🆕' },
LOCK: { type: 'emoji', value: '🔒' },
UNLOCK: { type: 'emoji', value: '🔓' }, // Entsperren
LOCKED_SECURE: { type: 'emoji', value: '🔐' }, // Sicher gesperrt (mit Schlüssel)
KEY: { type: 'emoji', value: '🔑' },
HISTORY: { type: 'emoji', value: '🔙' },
VOTES: { type: 'emoji', value: '🗳️' },
TRUST: { type: 'emoji', value: '🤝' }, // Trust-Lock
EMERGENCY: { type: 'emoji', value: '🆘' }, // Notfall-Entsperrung
HYGIENE: { type: 'emoji', value: '🚿' }, // Hygiene-Öffnung
FROZEN: { type: 'emoji', value: '❄️' }, // Eingefroren (zeitlich)
FROZEN_HARD: { type: 'emoji', value: '🧊' }, // Eingefroren (unlimitiert)
UNFREEZE: { type: 'emoji', value: '🌊' }, // Aufgetaut / Unfreeze
CODE_DIGITS: { type: 'emoji', value: '🔢' }, // Zahlenkombination / PIN-Länge
// ── CardLock ──────────────────────────────────────────────────────────
CARD: { type: 'emoji', value: '🃏' }, // Karte (standalone)
DICE: { type: 'emoji', value: '🎲' }, // Zufällig / Würfeln
// ── TimeLock / Spinning Wheel ──────────────────────────────────────────
SPINNING_WHEEL: { type: 'emoji', value: '🎡' }, // Glücksrad drehen
TASK_ACTIVE: { type: 'emoji', value: '🎯' }, // Aktuelle Aufgabe
CLOCK: { type: 'emoji', value: '🕐' }, // Uhr / Zeitpunkt
// ── Social ────────────────────────────────────────────────────────────
FEED: { type: 'emoji', value: '📰' },
@@ -55,12 +92,34 @@ window.ICONS = {
LOGOUT: { type: 'emoji', value: '⏏️' },
PROFILE: { type: 'emoji', value: '👤' },
HELP: { type: 'emoji', value: '❓' },
CONTACT: { type: 'emoji', value: '✉️' }, // Kontakt / E-Mail
// ── Medien / Dateien ──────────────────────────────────────────────────
PHOTO: { type: 'emoji', value: '📷' }, // Foto / Kamera
FILE_UPLOAD: { type: 'emoji', value: '📁' }, // Datei auswählen / Upload
TEMPLATE: { type: 'emoji', value: '📋' }, // Vorlage / Template
DOCUMENT: { type: 'emoji', value: '📄' }, // Dokument / Impressum
GUIDE: { type: 'emoji', value: '📖' }, // Anleitung / Hilfeseite
STATS: { type: 'emoji', value: '📊' }, // Statistik / Umfrage-Ergebnis
PACKAGE: { type: 'emoji', value: '📦' }, // Paket / Einladung
MAILBOX: { type: 'emoji', value: '📬' }, // Posteingang (Admin)
// ── Abo / Premium ─────────────────────────────────────────────────────
PREMIUM: { type: 'emoji', value: '⭐' }, // Abonnement / Premium
TROPHY: { type: 'emoji', value: '🏆' }, // Auszeichnung / Erfolg
PAYMENT: { type: 'emoji', value: '💳' }, // Zahlung / Abonnement
// ── TTLock / Technik ──────────────────────────────────────────────────
MOBILE: { type: 'emoji', value: '📱' }, // TTLock-App / Mobilgerät
CONNECTION: { type: 'emoji', value: '🔌' }, // Verbindung / Integration
GAMEPAD: { type: 'emoji', value: '🕹️' }, // Spielsteuerung
SHIELD: { type: 'emoji', value: '🛡️' }, // Sicherheit / Datenschutz
ADMIN_TOOLS: { type: 'emoji', value: '🔧' }, // Admin / Werkzeuge
// ── Aufgaben / Items ──────────────────────────────────────────────────
TOYS: { type: 'emoji', value: '➰' },
// ── Spielhistorie Spieltypen ────────────────────────────────────────
// Einfache Spieltypen
GAME_BDSM: { type: 'emoji', value: '⛓️' },
GAME_VANILLA: { type: 'emoji', value: '❤️' },

View File

@@ -330,14 +330,24 @@
function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
const lockDetailCache = {};
const lockTypeMap = {}; // lockId → 'CARDLOCK' | 'TIMELOCK'
// ── Meine Locks als Keyholder ──
async function loadLocks() {
try {
const res = await fetch('/keyholder/as-keyholder');
if (!res.ok) return;
const locks = await res.json();
const [clRes, tlRes] = await Promise.all([
fetch('/keyholder/as-keyholder'),
fetch('/keyholder/timelock/as-keyholder')
]);
const cardLocks = clRes.ok ? await clRes.json() : [];
const timeLocks = tlRes.ok ? await tlRes.json() : [];
const locks = [
...cardLocks.map(l => ({ ...l, lockType: 'CARDLOCK' })),
...timeLocks.map(l => ({ ...l, lockType: 'TIMELOCK' }))
];
locks.forEach(l => { lockTypeMap[l.lockId] = l.lockType; });
const grid = document.getElementById('locksGrid');
grid.innerHTML = '';
const empty = document.getElementById('locksEmpty');
@@ -348,7 +358,11 @@
? `<div class="lock-card-avatar"><img src="data:image/jpeg;base64,${l.lockeeProfilePic}" alt=""></div>`
: `<div class="lock-card-avatar">👤</div>`;
const startDate = l.startTime ? new Date(l.startTime).toLocaleDateString('de-DE') : '';
const frozenBadge = l.isFrozenByKeyholder ? ' · ❄️ Eingefroren' : '';
const frozenBadge = (l.isFrozenByKeyholder || l.isFrozen) ? ' · ❄️ Eingefroren' : '';
const emergencyBadge = l.emergencyUnlockRequested ? ' · 🆘 Notfall!' : '';
const line3 = l.lockType === 'TIMELOCK'
? `⏱ TimeLock · seit ${startDate}${frozenBadge}${emergencyBadge}`
: `🃏 ${l.totalCards} Karten · seit ${startDate}${frozenBadge}${emergencyBadge}`;
const card = document.createElement('div');
card.className = 'lock-card';
card.dataset.lockId = l.lockId;
@@ -358,7 +372,7 @@
<div class="lock-card-body">
<div class="lock-card-line1">${esc(l.lockeeName)}</div>
<div class="lock-card-line2">${esc(l.lockName)}</div>
<div class="lock-card-line3">🃏 ${l.totalCards} Karten · seit ${startDate}${frozenBadge}</div>
<div class="lock-card-line3">${line3}</div>
</div>
<span class="lock-toggle">▶</span>
</div>
@@ -382,9 +396,13 @@
const card = document.querySelector(`[data-lock-id="${lockId}"]`);
const body = card ? card.querySelector('.lock-detail-body') : null;
try {
const res = await fetch('/keyholder/as-keyholder/' + lockId);
const endpoint = lockTypeMap[lockId] === 'TIMELOCK'
? '/keyholder/timelock/as-keyholder/' + lockId
: '/keyholder/as-keyholder/' + lockId;
const res = await fetch(endpoint);
if (!res.ok) { if (body) body.textContent = 'Fehler beim Laden.'; return; }
const d = await res.json();
lockTypeMap[lockId] = d.lockType || lockTypeMap[lockId] || 'CARDLOCK';
lockDetailCache[lockId] = d;
if (body) {
body.innerHTML = buildDetailHtml(d);
@@ -404,7 +422,90 @@
}
}
function buildTimeLockDetailHtml(d) {
let html = `<div style="margin-bottom:0.75rem;">
<a href="/benutzer.html?userId=${d.lockeeId}" style="font-size:0.82rem;color:var(--color-primary);">Profil ansehen →</a>
</div>`;
// Zeitinfo
html += `<div class="detail-section"><div class="detail-section-title">TimeLock</div>`;
if (d.timeUp) {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value ok">✅ Entsperrbereit</span></div>`;
} else if (d.unlockTime) {
html += `<div class="detail-row">
<span class="detail-label">Entsperrt um</span>
<span class="detail-value">${new Date(d.unlockTime).toLocaleString('de-DE')}</span>
</div>`;
} else {
html += `<div class="detail-row"><span class="detail-label">Entsperrzeit</span><span class="detail-value" style="color:var(--color-muted);">Nicht sichtbar</span></div>`;
}
if (d.isFrozen) {
const fu = d.frozenUntil ? new Date(d.frozenUntil).toLocaleString('de-DE') : 'unlimitiert';
html += `<div class="detail-row"><span class="detail-label">❄️ Eingefroren bis</span><span class="detail-value danger">${fu}</span></div>`;
}
html += `</div>`;
// Verifikation
if (d.requiresVerification) {
html += `<div class="detail-section"><div class="detail-section-title">Verifikation heute</div>`;
if (!d.verificationDoneToday) {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value danger">Warte auf Verifikation</span></div>`;
} else if (!d.verificationMyVote) {
html += `<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value warn">Ausstehend &nbsp;<a href="#" class="prufen-link" style="font-size:0.82rem;color:var(--color-primary);font-weight:600;">Prüfen →</a></span>
</div>`;
} else if (d.verificationMyVote === 'upvote') {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value ok">✓ Erledigt</span></div>`;
} else {
html += `<div class="detail-row"><span class="detail-label">Status</span><span class="detail-value danger">✗ Abgelehnt</span></div>`;
}
html += `</div>`;
}
// Gestartet am
if (d.startTime) {
html += `<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.5rem;">
Gestartet am ${new Date(d.startTime).toLocaleDateString('de-DE')}
</div>`;
}
// Notfall-Banner
if (d.emergencyUnlockRequested && !d.keyholderRequestedUnlock) {
html += `<div style="display:flex;align-items:flex-start;gap:0.75rem;background:rgba(231,76,60,0.12);border:2px solid rgba(231,76,60,0.5);border-radius:10px;padding:1rem 1.1rem;margin-top:0.75rem;margin-bottom:0.5rem;">
<span style="font-size:1.4rem;flex-shrink:0;">🆘</span>
<div>
<div style="font-weight:700;font-size:0.95rem;color:#e74c3c;margin-bottom:0.25rem;">Notfall-Entsperrung angefordert!</div>
<div style="font-size:0.85rem;color:var(--color-muted);line-height:1.5;">Deine Lockee bittet dringend um Freigabe des Locks. Reagiere innerhalb einer Stunde oder das Lock öffnet sich automatisch.</div>
<div style="margin-top:0.6rem;">
<button onclick="requestUnlock('${d.lockId}')"
style="background:rgba(46,204,113,0.2);border:1px solid rgba(46,204,113,0.5);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.85rem;font-weight:600;width:auto;">
🔓 Jetzt freigeben
</button>
</div>
</div>
</div>`;
}
// Lock entsperren
html += `<div class="detail-section"><div class="detail-section-title">Lock entsperren</div>`;
if (d.keyholderRequestedUnlock) {
html += `<div style="font-size:0.85rem;color:var(--color-muted);">✅ Unlock wurde angefordert die Lockee erhält beim nächsten Laden ihren Entsperrcode.</div>`;
} else {
html += `<div id="unlockConfirm_${d.lockId}">
<button onclick="showUnlockConfirm('${d.lockId}')"
style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">
🔓 Lock freigeben
</button>
</div>`;
}
html += `</div>`;
return html;
}
function buildDetailHtml(d) {
if (d.lockType === 'TIMELOCK') return buildTimeLockDetailHtml(d);
let html = `<div style="margin-bottom:0.75rem;">
<a href="/benutzer.html?userId=${d.lockeeId}" style="font-size:0.82rem;color:var(--color-primary);">Profil ansehen →</a>
</div>`;
@@ -1103,7 +1204,10 @@
async function requestUnlock(lockId) {
try {
const res = await fetch(`/keyholder/as-keyholder/${lockId}/request-unlock`, { method: 'POST' });
const endpoint = (lockDetailCache[lockId]?.lockType || lockTypeMap[lockId]) === 'TIMELOCK'
? `/keyholder/timelock/as-keyholder/${lockId}/request-unlock`
: `/keyholder/as-keyholder/${lockId}/request-unlock`;
const res = await fetch(endpoint, { method: 'POST' });
if (res.ok || res.status === 204) {
await reloadLockDetail(lockId);
}

View File

@@ -175,6 +175,7 @@
display:flex; align-items:center; gap:0.5rem;
background:var(--color-card); border-radius:7px; padding:0.55rem 0.75rem;
flex-wrap:wrap;
border-left: 4px solid transparent; transition: border-color 0.15s;
}
.wheel-item select {
flex:1; min-width:150px; box-sizing:border-box;
@@ -650,6 +651,15 @@
{ value:'TASK', label:'Aufgabe zuweisen', hasInt:false, hasStr:false },
{ value:'TEXT', label:'Text anzeigen', hasInt:false, hasStr:true, strLabel:'Text' },
];
const WHEEL_TYPE_COLORS = {
ADD_TIME: '#f39c12',
REMOVE_TIME: '#27ae60',
FREEZE_TIME: '#3498db',
FREEZE: '#e74c3c',
UNFREEZE: '#27ae60',
TASK: '#e6b800',
TEXT: '#3498db',
};
function addWheelEntry(data) {
const id = ++wheelCtr;
@@ -684,8 +694,10 @@
const sel = document.querySelector(`#we-${id} select`);
if (!sel) return;
const def = WHEEL_TYPES.find(t => t.value === sel.value) || WHEEL_TYPES[0];
document.getElementById('we-tp-' + id).style.display = def.hasInt ? '' : 'none';
document.getElementById('we-str-' + id).style.display = def.hasStr ? '' : 'none';
document.getElementById('we-tp-' + id).style.display = def.hasInt ? '' : 'none';
document.getElementById('we-str-' + id).style.display = def.hasStr ? '' : 'none';
const item = document.getElementById('we-' + id);
if (item) item.style.borderLeftColor = WHEEL_TYPE_COLORS[sel.value] || 'transparent';
}
function removeWheelEntry(id) {
document.getElementById('we-' + id)?.remove();

View File

@@ -701,6 +701,38 @@
return cards;
}
// ── Plausibilitätsprüfung für TimeLock ──
function validateTimeLockPlausibility(t) {
const errors = [];
const hasTasks = t.tasks && t.tasks.length > 0;
const spinEntries = t.spinningWheelEntries || [];
// Spinning Wheel enthält Task-Felder, aber keine Aufgaben definiert
if (spinEntries.some(e => e.type === 'TASK') && !hasTasks) {
errors.push('Das Spinning Wheel enthält Aufgaben-Felder (TASK), aber die Vorlage hat keine Aufgaben definiert. Bitte die Vorlage bearbeiten.');
}
// Aufgaben-Häufigkeit konfiguriert, aber keine Aufgaben vorhanden
if ((t.taskEveryMinutes > 0 || t.minTasksPerDay > 0) && !hasTasks) {
errors.push('Aufgaben sind zeitlich konfiguriert, aber keine Aufgaben in der Vorlage definiert. Bitte die Vorlage bearbeiten.');
}
return errors;
}
function showPlausibilityErrors(errors) {
const el = document.getElementById('errorMsg');
if (errors.length === 1) {
el.textContent = errors[0];
} else {
el.innerHTML = 'Die Vorlage enthält inkonsistente Einstellungen:<ul style="margin:0.4rem 0 0 1.2rem;padding:0;">'
+ errors.map(e => `<li>${e}</li>`).join('')
+ '</ul>';
}
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// ── Absenden ──
async function createSession() {
document.getElementById('errorMsg').style.display = 'none';
@@ -716,6 +748,14 @@
const t = selectedTemplate || allTemplates.find(x => x.templateId === templateId);
if (!t) { showError('Vorlage nicht gefunden.'); return; }
if (t._type === 'timelock') {
const plausErrors = validateTimeLockPlausibility(t);
if (plausErrors.length > 0) {
showPlausibilityErrors(plausErrors);
return;
}
}
const lockeeVal = document.getElementById('lockeeValue').value;
const keyholderVal = document.getElementById('keyholderValue').value;
const isFriendLockee = lockeeVal && lockeeVal !== myUserId;