Weitere Fixes am Taskgame
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-05-03 20:08:58 +02:00
parent ca0e933d95
commit ae64ca6aa3
98 changed files with 868 additions and 505 deletions

View File

@@ -1,13 +1,4 @@
Umsetzung des Spiels:
Der Lockee hat eine Stunde Zeit das Spiel zu starten, dies geschieht per Knopfdruck
Wenn er dies nicht schafft -> bei Keyholder, benachrichtige Keyholder und lass sie/ihn entscheiden, ansonsten freeze wie bei freeze card
Übernimm die Logik des Spiels aus dem BDSM Game.
Falls eine Zeitstrafe eine temporäre Öffnung vor oder nach der Aufgabe benötigt, öffne das Lock für 5 Minuten. Überzogene Zeit wird addiert und am Ende des Locks gefreezed
Selbiges gilt, falls der finisher eine temporäre Öffnung danach erfoldert
Benötigt der Finisher eine Öffnung davor, verwende die Logik der Cum Card, und addiere diese Zeit auf die möglicherweise schon vorhandenen Freeze Zeit am Ende des Locks
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen, wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,
hier kann ich die auswählen, zu denen ich "Ich bin dabei" gedrückt habe, das hier kann ich die auswählen, zu denen ich "Ich bin dabei" gedrückt habe, das

View File

@@ -23,6 +23,7 @@
letter-spacing: 0.08em; letter-spacing: 0.08em;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
height: 1.2rem; height: 1.2rem;
text-align: center;
} }
.game-text { .game-text {
font-size: 1rem; font-size: 1rem;
@@ -31,6 +32,7 @@
white-space: pre-wrap; white-space: pre-wrap;
height: 14rem; height: 14rem;
overflow-y: auto; overflow-y: auto;
text-align: center;
} }
.game-timer { .game-timer {
font-size: 2.2rem; font-size: 2.2rem;
@@ -52,48 +54,9 @@
margin-top: 1rem; margin-top: 1rem;
height: 2.75rem; height: 2.75rem;
} }
#confirmModal { display:none; }
#confirmModal.open { display:flex; }
.game-requirements {
margin: 0.75rem 0 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.game-requirements-label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--color-muted);
margin-bottom: 0.1rem;
}
.req-check {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.4rem 0.6rem;
border-radius: 7px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--color-secondary);
cursor: pointer;
user-select: none;
transition: border-color 0.15s, background 0.15s;
font-size: 0.9rem;
color: var(--color-text);
}
.req-check input[type="checkbox"] {
accent-color: var(--color-primary);
width: 15px;
height: 15px;
flex-shrink: 0;
cursor: pointer;
}
.req-check.done {
border-color: var(--color-primary);
background: rgba(233,69,96,0.07);
color: var(--color-muted);
text-decoration: line-through;
}
.level-display { .level-display {
display: flex; display: flex;
@@ -106,16 +69,6 @@
object-fit: contain; object-fit: contain;
} }
.lock-messages {
background: rgba(233,69,96,0.1);
border: 1px solid rgba(233,69,96,0.3);
border-radius: 10px;
padding: 1rem 1.1rem;
margin-bottom: 1.25rem;
}
.lock-messages p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; }
.lock-messages p:last-child { margin: 0; }
.init-box { .init-box {
background: var(--color-card); background: var(--color-card);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
@@ -176,15 +129,13 @@
margin-top: 0.6rem; margin-top: 0.6rem;
} }
#finisherBox { #levelTrophy {
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12)); font-size: 3.5rem;
border: 1px solid rgba(233,69,96,0.4); line-height: 72px;
border-radius: 14px; width: 72px;
padding: 1.5rem; height: 72px;
text-align: center; text-align: center;
} }
#finisherBox .trophy { font-size: 2.5rem; margin-bottom: 0.5rem; }
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
</style> </style>
</head> </head>
<body class="app"> <body class="app">
@@ -195,11 +146,9 @@
<!-- Level-Anzeige --> <!-- Level-Anzeige -->
<div class="level-display" id="levelDisplay" style="display:none;"> <div class="level-display" id="levelDisplay" style="display:none;">
<img id="levelImg" src="" alt="Level"> <img id="levelImg" src="" alt="Level">
<span id="levelTrophy" style="display:none;">🏆</span>
</div> </div>
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
<!-- Toy-Auswahl vor Spielstart --> <!-- Toy-Auswahl vor Spielstart -->
<div id="toyBox" style="display:none;"> <div id="toyBox" style="display:none;">
<div class="game-card"> <div class="game-card">
@@ -232,8 +181,7 @@
<div id="gameCard" class="game-card" style="display:none;"> <div id="gameCard" class="game-card" style="display:none;">
<div class="game-label" id="gameLabel"></div> <div class="game-label" id="gameLabel"></div>
<div class="game-text" id="gameText"></div> <div class="game-text" id="gameText"></div>
<div id="gameRequirements" class="game-requirements" style="display:none;"></div> <div class="game-timer" id="gameTimer"></div>
<div class="game-timer" id="gameTimer"></div>
<div class="game-btn-row"> <div class="game-btn-row">
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button> <button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
</div> </div>
@@ -241,38 +189,50 @@
<!-- Release-Text (Sperren) --> <!-- Release-Text (Sperren) -->
<div id="lockReleaseBox" class="game-card" style="display:none;"> <div id="lockReleaseBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Sperre aufgehoben</div> <div class="game-label">🔓 Zeitstrafe verbüßt</div>
<div class="game-text" id="releaseText"></div> <div class="game-text" id="releaseText"></div>
<div style="margin-top:1.1rem;"> <div class="game-timer"></div>
<button class="btn-primary" id="btnReleaseOk">OK</button> <div class="game-btn-row">
<button class="btn-primary" id="btnReleaseOk" style="width:100%;height:100%;">OK</button>
</div> </div>
</div> </div>
<!-- Temporäre Öffnung --> <!-- Temporäre Öffnung -->
<div id="tempOpeningBox" class="game-card" style="display:none;"> <div id="tempOpeningBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Temporäre Öffnung erforderlich</div> <!-- Phase 1: Öffnung aktiv -->
<div class="game-text" id="tempOpeningTask"></div> <div id="tempPhase1">
<div id="tempOpeningCodeRow" style="display:none; margin-top:1rem; text-align:center;"> <div class="game-label" style="text-align:center;">🔓 Entsperrcode</div>
<div class="game-label">Entsperrcode</div> <div class="game-text" id="tempOpeningCode"
<div id="tempOpeningCode" style="font-size:1.8rem; font-weight:700; letter-spacing:0.18em; padding:0.6rem 0; color:var(--color-primary);"></div> style="display:flex;align-items:center;justify-content:center;
font-size:1.8rem;font-weight:700;letter-spacing:0.18em;
color:var(--color-primary);white-space:normal;"></div>
<div class="game-timer" id="tempOpeningTimer" style="margin-top:0.75rem;"></div>
<div class="game-btn-row">
<button class="btn-primary" style="width:100%;height:100%;" onclick="doEndTempOpening()">✓ Erledigt</button>
</div>
</div> </div>
<div class="game-btn-row"> <!-- Phase 2: Neuer Code zum Schließen -->
<button class="btn-primary" onclick="doEndTempOpening()">✓ Erledigt</button> <div id="tempPhase2" style="display:none;">
<div class="game-label" style="text-align:center;">🔒 Lock schließen</div>
<div class="game-text" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.75rem;white-space:normal;">
<span style="font-size:0.9rem;color:var(--color-muted);text-align:center;">Nutze den Entsperrcode um den Schlüssel wieder zu verschließen.</span>
<div id="tempNewCode" style="font-size:1.8rem;font-weight:700;letter-spacing:0.18em;color:var(--color-primary);text-align:center;"></div>
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;text-align:center;"></div>
</div>
<div class="game-timer"></div>
<div class="game-btn-row">
<button class="btn-primary" id="tempPhase2Btn" style="width:100%;height:100%;" onclick="startTempScramble()">OK</button>
</div>
</div> </div>
</div> </div>
<!-- Finisher --> <!-- Finisher -->
<div id="finisherBox" style="display:none;"> <div id="finisherBox" class="game-card" style="display:none;">
<div class="trophy">🏆</div> <div class="game-label" id="finisherLabel"></div>
<h2>Level 6 erreicht!</h2> <div class="game-text" id="finisherText"></div>
<div class="game-label" id="finisherTitle"></div> <div class="game-timer" id="finisherTimer"></div>
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div> <div class="game-btn-row">
<div id="finisherStart" style="margin-top:1.25rem;"> <button class="btn-primary" id="finisherBtn" style="width:100%;height:100%;"></button>
<button class="btn-primary" onclick="doStartFinisher()">▶ Starten</button>
</div>
<div id="finisherRunning" style="display:none;margin-top:1.25rem;">
<div class="game-timer active" id="finisherTimer">00:00</div>
<button class="btn-primary" onclick="doEndFinisher()" style="margin-top:1rem;">✓ Erledigt</button>
</div> </div>
</div> </div>
@@ -291,6 +251,17 @@
</div> </div>
</div> </div>
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:420px;">
<h2>Bestätigung</h2>
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Nein</button>
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Ja, abbrechen</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script> <script src="/js/icons.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script> <script>
@@ -302,6 +273,10 @@
let _state = null; let _state = null;
let _timerInt = null; let _timerInt = null;
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done' let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
let _tempOpeningTimerInt = null;
let _tempScrambleTimer = null;
let _tempScrambleCd = null;
let _tempOpeningFromQueue = false; // true only when opening was triggered from a queued Zeitstrafe (doQueueStart)
function goBack() { function goBack() {
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId; if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
@@ -416,6 +391,13 @@
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
hide('loadingHint'); hide('loadingHint');
// Remove ?fresh=1 from URL so F5 resumes instead of restarting.
const cleanParams = new URLSearchParams(location.search);
cleanParams.delete('fresh');
const cleanSearch = cleanParams.toString();
history.replaceState(null, '', location.pathname + (cleanSearch ? '?' + cleanSearch : ''));
await runGameLoop(); await runGameLoop();
} }
@@ -482,26 +464,7 @@
// ── Benötigt-Checkboxen ─────────────────────────────────────────────────── // ── Benötigt-Checkboxen ───────────────────────────────────────────────────
const WERKZEUG_LABEL = { // ── Game Loop ─────────────────────────────────────────────────────────────
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
};
function renderRequirements(list) {
const box = document.getElementById('gameRequirements');
if (!list || list.length === 0) { box.style.display = 'none'; box.innerHTML = ''; return; }
box.innerHTML = '<div class="game-requirements-label">Benötigt</div>' +
list.map(w => {
const label = WERKZEUG_LABEL[w] || w;
return `<label class="req-check" onclick="this.classList.toggle('done',this.querySelector('input').checked)">
<input type="checkbox" onchange="this.closest('.req-check').classList.toggle('done',this.checked)">
<span>${label}</span>
</label>`;
}).join('');
box.style.display = 'flex';
}
// ── Game Loop ─────────────────────────────────────────────────────────────
function setGameCard(label, text, action, btnLabel) { function setGameCard(label, text, action, btnLabel) {
document.getElementById('gameLabel').textContent = label; document.getElementById('gameLabel').textContent = label;
@@ -514,20 +477,26 @@
} }
async function runGameLoop() { async function runGameLoop() {
hide('gameCard');
hide('finisherBox');
hide('tempOpeningBox');
clearTimer(); clearTimer();
if (_state.tempOpeningTime) {
hide('finisherBox');
showTempOpeningDialog();
return;
}
if (_state.finisher) { if (_state.finisher) {
hide('tempOpeningBox');
showFinisherUI(); showFinisherUI();
return; return;
} }
if (_state.tempOpeningTime) { // Normal game states all use gameCard — show it once here so the card
showTempOpeningDialog(); // frame stays stable across transitions (queue ↔ active ↔ active-done).
return; show('gameBox');
} show('gameCard');
hide('tempOpeningBox');
hide('finisherBox');
renderLevelBar(_state.level); renderLevelBar(_state.level);
@@ -549,29 +518,25 @@
if (_state.lockInQueue) { if (_state.lockInQueue) {
let sperre; let sperre;
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; } try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten'); const btnLabel = sperre.tempUnlockRequired ? '⏱ Weiter zur temporären Öffnung' : '✓ Erledigt';
renderRequirements(null); setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', btnLabel);
} else if (_state.taskInQueue) { } else if (_state.taskInQueue) {
let aufgabe; let aufgabe;
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; } try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis); const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done', setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
hasDuration ? '▶ Starten' : '✓ Erledigt'); hasDuration ? '▶ Starten' : '✓ Erledigt');
renderRequirements(aufgabe.benoetigtAktiv);
} }
show('gameBox');
show('gameCard');
} }
function showActiveTask(text, endIso) { function showActiveTask(text, endIso) {
show('gameBox');
show('gameCard');
const timerEl = document.getElementById('gameTimer'); const timerEl = document.getElementById('gameTimer');
timerEl.classList.remove('active', 'urgent'); timerEl.classList.remove('active', 'urgent');
timerEl.textContent = ''; timerEl.textContent = '';
document.getElementById('gameLabel').textContent = 'Aktive Aufgabe'; document.getElementById('gameLabel').textContent = 'Aktive Aufgabe';
document.getElementById('gameText').textContent = text; document.getElementById('gameText').textContent = text;
renderRequirements(_state.activeTaskBenoetigtAktiv);
if (endIso) { if (endIso) {
const end = new Date(endIso); const end = new Date(endIso);
@@ -590,7 +555,7 @@
switch (_gameAction) { switch (_gameAction) {
case 'queue-start': doQueueStart(); break; case 'queue-start': doQueueStart(); break;
case 'queue-done': doQueueDone(); break; case 'queue-done': doQueueDone(); break;
case 'active-running': doCancelCountdown(); break; case 'active-running': openConfirmModal('Aufgabe wirklich abbrechen?', () => doCancelCountdown()); break;
case 'active-done': doErledigt(); break; case 'active-done': doErledigt(); break;
} }
} }
@@ -603,21 +568,21 @@
try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {} try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {}
} }
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten'); return; }
if (wasLock && tempUnlockRequired) { if (wasLock && tempUnlockRequired) {
await fetch('/lock-game/start-temp-opening', { method: 'POST' }); // Erst Öffnung starten, Zeitstrafe wird nach der Öffnung angewendet
const r = await fetch('/lock-game/start-temp-opening', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten der Öffnung'); return; }
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
_tempOpeningFromQueue = true;
showTempOpeningDialog(); showTempOpeningDialog();
} else if (wasLock) {
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
} else { } else {
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten'); return; }
if (wasLock) {
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
}
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
await runGameLoop(); await runGameLoop();
@@ -627,7 +592,13 @@
async function doQueueDone() { async function doQueueDone() {
try { try {
await checkAndShowLocks(); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' }); const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden'); return; } if (!applyR.ok) { showError('Fehler beim Anwenden'); return; }
const nextR = await fetch('/lock-game/next-task', { method: 'POST' }); const nextR = await fetch('/lock-game/next-task', { method: 'POST' });
@@ -642,9 +613,9 @@
clearTimer(); clearTimer();
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' }); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) { if (lockR.ok) {
const texts = await lockR.json(); const sperren = await lockR.json();
for (const text of (texts || [])) { for (const sperre of (sperren || [])) {
if (text != null && text !== '') await waitForReleaseOk(text); if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
} }
} }
_gameAction = 'active-done'; _gameAction = 'active-done';
@@ -655,9 +626,9 @@
try { try {
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' }); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) { if (lockR.ok) {
const texts = await lockR.json(); const sperren = await lockR.json();
for (const text of (texts || [])) { for (const sperre of (sperren || [])) {
if (text != null && text !== '') await waitForReleaseOk(text); if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
} }
} }
const r = await fetch('/lock-game/next-task', { method: 'POST' }); const r = await fetch('/lock-game/next-task', { method: 'POST' });
@@ -668,64 +639,149 @@
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); } } catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
} }
async function checkAndShowLocks() {
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
if (!r.ok) return;
const texts = await r.json();
const valid = texts ? texts.filter(t => t != null && t !== '') : [];
if (valid.length > 0) {
const box = document.getElementById('lockMessages');
box.innerHTML = valid.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
show('lockMessages');
await new Promise(res => setTimeout(res, 2000));
}
}
function showTempOpeningDialog() { function showTempOpeningDialog() {
show('gameBox'); show('gameBox');
hide('gameCard'); hide('gameCard');
hide('lockReleaseBox'); hide('lockReleaseBox');
hide('finisherBox'); hide('finisherBox');
document.getElementById('tempOpeningTask').textContent = _state.activeTask || ''; document.getElementById('tempOpeningCode').textContent = _state.tempOpeningCode || '';
const code = _state.tempOpeningCode;
if (code) { show('tempPhase1');
document.getElementById('tempOpeningCode').textContent = code; hide('tempPhase2');
show('tempOpeningCodeRow');
} else { const timerEl = document.getElementById('tempOpeningTimer');
hide('tempOpeningCodeRow'); timerEl.textContent = '';
timerEl.classList.remove('active', 'urgent');
if (_state.tempOpeningTime && _state.tempOpeningDuration) {
const endTime = new Date(new Date(_state.tempOpeningTime).getTime() + _state.tempOpeningDuration * 60000);
if (endTime > Date.now()) startTempOpeningTimer(endTime, timerEl);
} }
show('tempOpeningBox'); show('tempOpeningBox');
} }
function startTempOpeningTimer(endDate, el) {
if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
el.classList.add('active');
function tick() {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s;
el.classList.toggle('urgent', diff < 30);
if (diff === 0) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
}
tick();
_tempOpeningTimerInt = setInterval(tick, 1000);
}
async function doEndTempOpening() { async function doEndTempOpening() {
try { try {
await fetch('/lock-game/end-temp-opening', { method: 'POST' }); if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
await fetch('/lock-game/abandon-task', { method: 'POST' });
const stateR = await fetch('/lock-game/state'); // Only apply the queued Zeitstrafe when the opening was explicitly started from one (Path 1).
_state = await stateR.json(); // When triggered by a background lock expiry via checkLocks() (Path 2), lockInQueue may be set
hide('tempOpeningBox'); // to the *next* randomly-picked item — applying it would incorrectly create another background
await runGameLoop(); // lock with tempUnlockRequired and produce an infinite opening loop.
const fromQueue = _tempOpeningFromQueue;
_tempOpeningFromQueue = false;
const hasLockInQueue = fromQueue && !!_state.lockInQueue;
if (hasLockInQueue) {
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden der Zeitstrafe'); return; }
}
const endR = await fetch('/lock-game/end-temp-opening', { method: 'POST' });
if (!endR.ok) { showError('Fehler beim Beenden der Öffnung'); return; }
const endData = await endR.json();
const newCode = endData.newUnlockCode;
if (newCode) {
document.getElementById('tempNewCode').textContent = newCode;
document.getElementById('tempScrambleCountdown').style.display = 'none';
document.getElementById('tempPhase2Btn').textContent = 'OK';
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
hide('tempPhase1');
show('tempPhase2');
} else {
await finishTempOpening();
}
} catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); } } catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
} }
async function finishTempOpening() {
hide('tempOpeningBox');
await fetch('/lock-game/abandon-task', { method: 'POST' });
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
}
function startTempScramble() {
const codeEl = document.getElementById('tempNewCode');
const cdEl = document.getElementById('tempScrambleCountdown');
const btnEl = document.getElementById('tempPhase2Btn');
const len = codeEl.textContent.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
async function finish() {
stopped = true;
clearInterval(_tempScrambleTimer); _tempScrambleTimer = null;
clearInterval(_tempScrambleCd); _tempScrambleCd = null;
await finishTempOpening();
}
cdEl.style.display = '';
btnEl.textContent = 'Abbrechen';
btnEl.onclick = finish;
function updateCd() {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
cdEl.textContent = `${m}:${String(s).padStart(2, '0')}`;
}
updateCd();
_tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
_tempScrambleCd = setInterval(() => {
if (stopped) return;
if (--remaining <= 0) finish();
else updateCd();
}, 1000);
}
function showFinisherUI() { function showFinisherUI() {
show('gameBox'); show('gameBox');
hide('gameCard'); hide('gameCard');
hide('lockReleaseBox'); hide('lockReleaseBox');
hide('tempOpeningBox');
renderLevelBar(6);
let finisher = {}; let finisher = {};
try { finisher = JSON.parse(_state.finisher); } catch (_) {} try { finisher = JSON.parse(_state.finisher); } catch (_) {}
document.getElementById('finisherTitle').textContent = finisher.kurzText || ''; document.getElementById('finisherLabel').textContent = finisher.kurzText || '';
document.getElementById('finisherText').textContent = finisher.text || ''; document.getElementById('finisherText').textContent = finisher.text || '';
const timerEl = document.getElementById('finisherTimer');
const btnEl = document.getElementById('finisherBtn');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
if (_state.finisherStartedAt) { if (_state.finisherStartedAt) {
hide('finisherStart'); timerEl.classList.add('active');
show('finisherRunning');
startElapsedTimer(new Date(_state.finisherStartedAt)); startElapsedTimer(new Date(_state.finisherStartedAt));
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
} else { } else {
show('finisherStart'); btnEl.textContent = '▶ Starten';
hide('finisherRunning'); btnEl.onclick = doStartFinisher;
} }
show('finisherBox'); show('finisherBox');
} }
@@ -734,9 +790,12 @@
await fetch('/lock-game/start-finisher', { method: 'POST' }); await fetch('/lock-game/start-finisher', { method: 'POST' });
const r = await fetch('/lock-game/state'); const r = await fetch('/lock-game/state');
_state = await r.json(); _state = await r.json();
hide('finisherStart'); const timerEl = document.getElementById('finisherTimer');
show('finisherRunning'); timerEl.classList.add('active');
startElapsedTimer(new Date(_state.finisherStartedAt)); startElapsedTimer(new Date(_state.finisherStartedAt));
const btnEl = document.getElementById('finisherBtn');
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
} }
async function doEndFinisher() { async function doEndFinisher() {
@@ -750,12 +809,14 @@
function startElapsedTimer(startDate) { function startElapsedTimer(startDate) {
clearTimer(); clearTimer();
const el = document.getElementById('finisherTimer'); const el = document.getElementById('finisherTimer');
_timerInt = setInterval(() => { function tick() {
const diff = Math.floor((Date.now() - startDate) / 1000); const diff = Math.floor((Date.now() - startDate) / 1000);
const m = String(Math.floor(diff / 60)).padStart(2, '0'); const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0'); const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s; el.textContent = m + ':' + s;
}, 1000); }
tick();
_timerInt = setInterval(tick, 1000);
} }
function waitForReleaseOk(text) { function waitForReleaseOk(text) {
@@ -773,8 +834,12 @@
// ── Level-Bar ───────────────────────────────────────────────────────────── // ── Level-Bar ─────────────────────────────────────────────────────────────
function renderLevelBar(level) { function renderLevelBar(level) {
const lvl = Math.min(Math.max(level, 1), 5); const isFinisher = level >= 6;
document.getElementById('levelImg').src = `/img/lvl${lvl}.png`; document.getElementById('levelImg').style.display = isFinisher ? 'none' : '';
document.getElementById('levelTrophy').style.display = isFinisher ? '' : 'none';
if (!isFinisher) {
document.getElementById('levelImg').src = `/img/lvl${Math.min(Math.max(level, 1), 5)}.png`;
}
show('levelDisplay'); show('levelDisplay');
} }
@@ -787,7 +852,7 @@
function startTimer(endDate, el) { function startTimer(endDate, el) {
el.classList.add('active'); el.classList.add('active');
clearTimer(); clearTimer();
_timerInt = setInterval(() => { function tick() {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000)); const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
const m = String(Math.floor(diff / 60)).padStart(2, '0'); const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0'); const s = String(diff % 60).padStart(2, '0');
@@ -799,7 +864,9 @@
_gameAction = 'active-done'; _gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt'; document.getElementById('gameBtn').textContent = '✓ Erledigt';
} }
}, 1000); }
tick();
_timerInt = setInterval(tick, 1000);
} }
function clearTimer() { function clearTimer() {
@@ -818,6 +885,25 @@
box.style.display = ''; box.style.display = '';
} }
// ── Confirm Modal ─────────────────────────────────────────────────────────
const _confirmModal = document.getElementById('confirmModal');
document.getElementById('confirmModalCancel').addEventListener('click', closeConfirmModal);
document.getElementById('confirmModalOk').addEventListener('click', closeConfirmModal);
_confirmModal.addEventListener('click', e => { if (e.target === _confirmModal) closeConfirmModal(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && _confirmModal.classList.contains('open')) closeConfirmModal(); });
function closeConfirmModal() { _confirmModal.classList.remove('open'); }
function openConfirmModal(text, onConfirm) {
document.getElementById('confirmModalText').textContent = text;
const okBtn = document.getElementById('confirmModalOk');
const newOk = okBtn.cloneNode(true);
okBtn.parentNode.replaceChild(newOk, okBtn);
newOk.addEventListener('click', () => { closeConfirmModal(); onConfirm(); });
_confirmModal.classList.add('open');
}
boot(); boot();
</script> </script>
</body> </body>

View File

@@ -1,13 +1,4 @@
Umsetzung des Spiels:
Der Lockee hat eine Stunde Zeit das Spiel zu starten, dies geschieht per Knopfdruck
Wenn er dies nicht schafft -> bei Keyholder, benachrichtige Keyholder und lass sie/ihn entscheiden, ansonsten freeze wie bei freeze card
Übernimm die Logik des Spiels aus dem BDSM Game.
Falls eine Zeitstrafe eine temporäre Öffnung vor oder nach der Aufgabe benötigt, öffne das Lock für 5 Minuten. Überzogene Zeit wird addiert und am Ende des Locks gefreezed
Selbiges gilt, falls der finisher eine temporäre Öffnung danach erfoldert
Benötigt der Finisher eine Öffnung davor, verwende die Logik der Cum Card, und addiere diese Zeit auf die möglicherweise schon vorhandenen Freeze Zeit am Ende des Locks
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen, wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,
hier kann ich die auswählen, zu denen ich "Ich bin dabei" gedrückt habe, das hier kann ich die auswählen, zu denen ich "Ich bin dabei" gedrückt habe, das

View File

@@ -1,38 +1,43 @@
package de.oaa.xxx.config; package de.oaa.xxx.config;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import org.slf4j.Logger;
import java.util.List; import org.slf4j.LoggerFactory;
@Converter import java.util.List;
public class StringListConverter implements AttributeConverter<List<String>, String> {
@Converter
private static final ObjectMapper mapper = new ObjectMapper(); public class StringListConverter implements AttributeConverter<List<String>, String> {
@Override private static final Logger LOGGER = LoggerFactory.getLogger(StringListConverter.class);
public String convertToDatabaseColumn(List<String> list) { private static final ObjectMapper mapper = new ObjectMapper();
if (list == null || list.isEmpty()) return null;
try { @Override
return mapper.writeValueAsString(list); public String convertToDatabaseColumn(List<String> list) {
} catch (Exception e) { if (list == null || list.isEmpty()) return null;
return null; try {
} return mapper.writeValueAsString(list);
} } catch (Exception e) {
LOGGER.warn("Fehler beim Serialisieren der String-Liste in DB-Spalte", e);
@Override return null;
public List<String> convertToEntityAttribute(String json) { }
if (json == null || json.isBlank()) return List.of(); }
try {
if (!json.startsWith("[")) { @Override
// Legacy: single base64 string public List<String> convertToEntityAttribute(String json) {
return List.of(json); if (json == null || json.isBlank()) return List.of();
} try {
return mapper.readValue(json, new TypeReference<>() {}); if (!json.startsWith("[")) {
} catch (Exception e) { // Legacy: single base64 string
return List.of(); return List.of(json);
} }
} return mapper.readValue(json, new TypeReference<>() {});
} } catch (Exception e) {
LOGGER.warn("Fehler beim Deserialisieren der String-Liste aus DB-Spalte: {}", json, e);
return List.of();
}
}
}

View File

@@ -3,6 +3,8 @@ package de.oaa.xxx.dating;
import de.oaa.xxx.social.SseService; import de.oaa.xxx.social.SseService;
import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.user.Geschlecht; import de.oaa.xxx.user.Geschlecht;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.user.Neigung; import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@@ -24,6 +26,8 @@ import java.util.stream.Collectors;
@RequestMapping("/dating") @RequestMapping("/dating")
public class DatingController { public class DatingController {
private static final Logger LOGGER = LoggerFactory.getLogger(DatingController.class);
private static final Set<VorliebeBewertung> POSITIVE_RATINGS = Set.of( private static final Set<VorliebeBewertung> POSITIVE_RATINGS = Set.of(
VorliebeBewertung.MAG_ICH, VorliebeBewertung.UNBEDINGT, VorliebeBewertung.WILL_AUSPROBIEREN); VorliebeBewertung.MAG_ICH, VorliebeBewertung.UNBEDINGT, VorliebeBewertung.WILL_AUSPROBIEREN);
@@ -254,7 +258,10 @@ public class DatingController {
return values.stream() return values.stream()
.map(s -> { .map(s -> {
try { return Enum.valueOf(enumClass, s); } try { return Enum.valueOf(enumClass, s); }
catch (IllegalArgumentException e) { return null; } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Enum-Wert in Dating: {}", s);
return null;
}
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.toList(); .toList();

View File

@@ -4,6 +4,8 @@ import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.entity.MessageCause; import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.repository.BlockRepository; import de.oaa.xxx.social.repository.BlockRepository;
import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.subscription.SubscriptionLimitService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.user.Geschlecht; import de.oaa.xxx.user.Geschlecht;
import de.oaa.xxx.user.Neigung; import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserEntity;
@@ -24,6 +26,8 @@ import java.util.stream.Collectors;
@RequestMapping("/dating/dates") @RequestMapping("/dating/dates")
public class DatingDateController { public class DatingDateController {
private static final Logger LOGGER = LoggerFactory.getLogger(DatingDateController.class);
private static final int MAX_DATES_STANDARD = 1; private static final int MAX_DATES_STANDARD = 1;
private static final int MAX_DATES_PRO = 5; private static final int MAX_DATES_PRO = 5;
@@ -388,7 +392,10 @@ public class DatingDateController {
return values.stream() return values.stream()
.map(s -> { .map(s -> {
try { return Enum.valueOf(enumClass, s); } try { return Enum.valueOf(enumClass, s); }
catch (IllegalArgumentException e) { return null; } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Enum-Wert in DatingDate: {}", s);
return null;
}
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.toList(); .toList();

View File

@@ -102,6 +102,7 @@ public class EmailChangeController {
try { try {
tokenId = UUID.fromString(token); tokenId = UUID.fromString(token);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültige E-Mail-Adresse beim Ändern: {}", e.getMessage());
response.sendRedirect("/login.html"); response.sendRedirect("/login.html");
return; return;
} }

View File

@@ -147,6 +147,7 @@ public class FeedController extends BaseController {
try { try {
typ = BeitragTyp.valueOf(req.beitragTyp()); typ = BeitragTyp.valueOf(req.beitragTyp());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Ungültiger BeitragTyp: {}", req.beitragTyp());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
@@ -202,6 +203,7 @@ public class FeedController extends BaseController {
try { try {
typ = BeitragTyp.valueOf(req.beitragTyp()); typ = BeitragTyp.valueOf(req.beitragTyp());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Ungültiger BeitragTyp: {}", req.beitragTyp());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }

View File

@@ -4,6 +4,8 @@ import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService; import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.support.SupportUserService; import de.oaa.xxx.support.SupportUserService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -20,6 +22,8 @@ import java.util.concurrent.ConcurrentHashMap;
@RequestMapping("/api/feedback") @RequestMapping("/api/feedback")
public class FeedbackController { public class FeedbackController {
private static final Logger LOGGER = LoggerFactory.getLogger(FeedbackController.class);
private final MailService mailService; private final MailService mailService;
private final FeedbackRepository feedbackRepository; private final FeedbackRepository feedbackRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
@@ -104,7 +108,7 @@ public class FeedbackController {
); );
mailService.send(email); mailService.send(email);
} catch (Exception e) { } catch (Exception e) {
// Mail-Server nicht erreichbar Eintrag ist bereits gespeichert LOGGER.warn("Mail-Server nicht erreichbar Feedback-Eintrag gespeichert, E-Mail nicht versandt", e);
} }
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();

View File

@@ -199,8 +199,10 @@ public class AufgabenGruppeController {
aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId()); aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId());
return ResponseEntity.status(201).build(); return ResponseEntity.status(201).build();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
LOGGER.warn("Konflikt beim Speichern der AufgabenGruppe: {}", e.getMessage());
return ResponseEntity.status(409).build(); return ResponseEntity.status(409).build();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Wert in AufgabenGruppe: {}", e.getMessage());
String msg = e.getMessage(); String msg = e.getMessage();
if (msg != null && msg.contains("nicht gefunden")) return ResponseEntity.notFound().build(); if (msg != null && msg.contains("nicht gefunden")) return ResponseEntity.notFound().build();
return ResponseEntity.status(403).build(); return ResponseEntity.status(403).build();

View File

@@ -567,10 +567,12 @@ public class BdsmGameController extends BaseController {
response.put("unlockCode", newLock.getUnlockCode()); response.put("unlockCode", newLock.getUnlockCode());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Parameter in BdsmGame: {}", e.getMessage());
String msg = e.getMessage(); String msg = e.getMessage();
if (msg != null && msg.contains("Session")) return ResponseEntity.notFound().build(); if (msg != null && msg.contains("Session")) return ResponseEntity.notFound().build();
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
LOGGER.warn("Konflikt in BdsmGame: {}", e.getMessage());
return ResponseEntity.status(409).build(); return ResponseEntity.status(409).build();
} }
} }

View File

@@ -4,6 +4,8 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@@ -11,6 +13,7 @@ import java.util.Map;
@Converter @Converter
public class CardCountMapConverter implements AttributeConverter<Map<String, Integer>, String> { public class CardCountMapConverter implements AttributeConverter<Map<String, Integer>, String> {
private static final Logger LOGGER = LoggerFactory.getLogger(CardCountMapConverter.class);
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
@Override @Override
@@ -19,6 +22,7 @@ public class CardCountMapConverter implements AttributeConverter<Map<String, Int
try { try {
return mapper.writeValueAsString(map); return mapper.writeValueAsString(map);
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Fehler beim Serialisieren der CardCount-Map in DB-Spalte", e);
return null; return null;
} }
} }
@@ -29,6 +33,7 @@ public class CardCountMapConverter implements AttributeConverter<Map<String, Int
try { try {
return mapper.readValue(json, new TypeReference<>() {}); return mapper.readValue(json, new TypeReference<>() {});
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Fehler beim Deserialisieren der CardCount-Map aus DB-Spalte", e);
return new LinkedHashMap<>(); return new LinkedHashMap<>();
} }
} }

View File

@@ -1,36 +1,41 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import org.slf4j.Logger;
import java.util.ArrayList; import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.ArrayList;
@Converter import java.util.List;
public class CardEnumListConverter implements AttributeConverter<List<CardEnum>, String> {
@Converter
private static final ObjectMapper mapper = new ObjectMapper(); public class CardEnumListConverter implements AttributeConverter<List<CardEnum>, String> {
@Override private static final Logger LOGGER = LoggerFactory.getLogger(CardEnumListConverter.class);
public String convertToDatabaseColumn(List<CardEnum> list) { private static final ObjectMapper mapper = new ObjectMapper();
if (list == null || list.isEmpty()) return null;
try { @Override
return mapper.writeValueAsString(list.stream().map(Enum::name).toList()); public String convertToDatabaseColumn(List<CardEnum> list) {
} catch (Exception e) { if (list == null || list.isEmpty()) return null;
return null; try {
} return mapper.writeValueAsString(list.stream().map(Enum::name).toList());
} } catch (Exception e) {
LOGGER.warn("Fehler beim Serialisieren der CardEnum-Liste in DB-Spalte", e);
@Override return null;
public List<CardEnum> convertToEntityAttribute(String json) { }
if (json == null || json.isBlank()) return new ArrayList<>(); }
try {
List<String> names = mapper.readValue(json, new TypeReference<>() {}); @Override
return new ArrayList<>(names.stream().map(CardEnum::valueOf).toList()); public List<CardEnum> convertToEntityAttribute(String json) {
} catch (Exception e) { if (json == null || json.isBlank()) return new ArrayList<>();
return new ArrayList<>(); try {
} List<String> names = mapper.readValue(json, new TypeReference<>() {});
} return new ArrayList<>(names.stream().map(CardEnum::valueOf).toList());
} } catch (Exception e) {
LOGGER.warn("Fehler beim Deserialisieren der CardEnum-Liste aus DB-Spalte", e);
return new ArrayList<>();
}
}
}

View File

@@ -61,11 +61,15 @@ import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService; import de.oaa.xxx.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController @RestController
@RequestMapping("/keyholder") @RequestMapping("/keyholder")
public class CardLockController { public class CardLockController {
private static final Logger LOGGER = LoggerFactory.getLogger(CardLockController.class);
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final KeyholderInvitationRepository invitationRepository; private final KeyholderInvitationRepository invitationRepository;
@@ -1174,6 +1178,7 @@ public class CardLockController {
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
toAdd.add(type); toAdd.add(type);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger CardEnum-Wert beim Hinzufügen von Karten: {}", entry.getKey());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
} }
@@ -1238,6 +1243,7 @@ public class CardLockController {
} }
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger CardEnum-Wert beim Entfernen von Karten");
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
} }
@@ -1477,6 +1483,7 @@ public class CardLockController {
try { try {
until = LocalDateTime.parse(req.until()); until = LocalDateTime.parse(req.until());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Ungültiges Datumsformat für SpeedEffect: {}", req.until());
return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat.")); return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat."));
} }
if (!until.isAfter(LocalDateTime.now())) if (!until.isAfter(LocalDateTime.now()))

View File

@@ -3,6 +3,8 @@ package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.tasks.TaskMode; import de.oaa.xxx.games.chastity.tasks.TaskMode;
import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.user.UserService; import de.oaa.xxx.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -18,6 +20,8 @@ import java.util.stream.Collectors;
@RequestMapping("/cardlock/templates") @RequestMapping("/cardlock/templates")
public class CardlockTemplateController { public class CardlockTemplateController {
private static final Logger LOGGER = LoggerFactory.getLogger(CardlockTemplateController.class);
private final CardlockTemplateRepository templateRepository; private final CardlockTemplateRepository templateRepository;
private final UserService userService; private final UserService userService;
private final TimeLockTemplateRepository timeLockTemplateRepository; private final TimeLockTemplateRepository timeLockTemplateRepository;
@@ -152,6 +156,7 @@ public class CardlockTemplateController {
.data(Map.of("min", min, "max", max, "avg", sum / total), MediaType.APPLICATION_JSON)); .data(Map.of("min", min, "max", max, "avg", sum / total), MediaType.APPLICATION_JSON));
emitter.complete(); emitter.complete();
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Fehler beim Senden des SSE-Events für Cardlock-Template: {}", e.getMessage());
emitter.completeWithError(e); emitter.completeWithError(e);
} }
}).start(); }).start();

View File

@@ -1,13 +1,14 @@
package de.oaa.xxx.games.chastity.common; package de.oaa.xxx.games.chastity.common;
import java.security.Principal; import java.security.Principal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -25,6 +26,7 @@ import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.games.common.aufgaben.AufgabenList; import de.oaa.xxx.games.common.aufgaben.AufgabenList;
import de.oaa.xxx.games.common.aufgaben.AvailableIn; import de.oaa.xxx.games.common.aufgaben.AvailableIn;
import de.oaa.xxx.games.common.aufgaben.Sperre;
import de.oaa.xxx.games.common.entity.AufgabeEntity; import de.oaa.xxx.games.common.entity.AufgabeEntity;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity; import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.entity.FinisherEntity; import de.oaa.xxx.games.common.entity.FinisherEntity;
@@ -40,6 +42,7 @@ import de.oaa.xxx.user.UserService;
@RequestMapping("/lock-game") @RequestMapping("/lock-game")
public class LockGameController { public class LockGameController {
private static final Logger LOGGER = LoggerFactory.getLogger(LockGameController.class);
private static final int AUFGABEN_PRO_LEVEL = 3; private static final int AUFGABEN_PRO_LEVEL = 3;
private final LockGameRepository lockGameRepository; private final LockGameRepository lockGameRepository;
@@ -231,6 +234,7 @@ public class LockGameController {
return ResponseEntity.ok(Map.of("gameId", game.getGameId().toString())); return ResponseEntity.ok(Map.of("gameId", game.getGameId().toString()));
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Initialisieren des LockGames", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }
@@ -243,6 +247,7 @@ public class LockGameController {
try { try {
return ResponseEntity.ok(buildService(opt.get()).getGameState()); return ResponseEntity.ok(buildService(opt.get()).getGameState());
} catch (Exception ex) { } catch (Exception ex) {
LOGGER.error("Fehler beim Laden des GameState", ex);
return ResponseEntity.internalServerError().build(); return ResponseEntity.internalServerError().build();
} }
} }
@@ -256,6 +261,7 @@ public class LockGameController {
buildService(opt.get()).initNextTask(); buildService(opt.get()).initNextTask();
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Ziehen der nächsten Aufgabe", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }
@@ -269,6 +275,7 @@ public class LockGameController {
buildService(opt.get()).abandonActiveTask(); buildService(opt.get()).abandonActiveTask();
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Abbrechen der aktiven Aufgabe", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }
@@ -282,30 +289,33 @@ public class LockGameController {
Integer seconds = buildService(opt.get()).applyTaskInQueue(); Integer seconds = buildService(opt.get()).applyTaskInQueue();
return ResponseEntity.ok(Map.of("seconds", seconds != null ? seconds : 0)); return ResponseEntity.ok(Map.of("seconds", seconds != null ? seconds : 0));
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Anwenden der Aufgabe aus der Queue", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }
@PostMapping("/check-locks") @PostMapping("/check-locks")
public ResponseEntity<List<String>> checkLocks(Principal principal) { public ResponseEntity<List<Sperre>> checkLocks(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId); var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.ok(List.of()); if (opt.isEmpty()) return ResponseEntity.ok(List.of());
try { try {
return ResponseEntity.ok(buildService(opt.get()).checkLocks()); return ResponseEntity.ok(buildService(opt.get()).checkLocks());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Fehler beim Prüfen abgelaufener Sperren", e);
return ResponseEntity.ok(List.of()); return ResponseEntity.ok(List.of());
} }
} }
@GetMapping("/release-locks") @GetMapping("/release-locks")
public ResponseEntity<List<String>> releaseLocks(Principal principal) { public ResponseEntity<List<Sperre>> releaseLocks(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId); var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.ok(List.of()); if (opt.isEmpty()) return ResponseEntity.ok(List.of());
try { try {
return ResponseEntity.ok(buildService(opt.get()).releaseLocks()); return ResponseEntity.ok(buildService(opt.get()).releaseLocks());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Fehler beim Auflösen aller Sperren", e);
return ResponseEntity.ok(List.of()); return ResponseEntity.ok(List.of());
} }
} }
@@ -319,6 +329,7 @@ public class LockGameController {
buildService(opt.get()).startFinisher(); buildService(opt.get()).startFinisher();
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Starten des Finishers", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }
@@ -332,6 +343,7 @@ public class LockGameController {
buildService(opt.get()).endFinisher(); buildService(opt.get()).endFinisher();
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Beenden des Finishers", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }
@@ -346,6 +358,7 @@ public class LockGameController {
buildService(opt.get()).startTempOpening(); buildService(opt.get()).startTempOpening();
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Starten der temporären Öffnung", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }
@@ -357,9 +370,10 @@ public class LockGameController {
var opt = lockGameRepository.findByUserId(userId); var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build(); if (opt.isEmpty()) return ResponseEntity.notFound().build();
try { try {
buildService(opt.get()).endTempOpening(); String newCode = buildService(opt.get()).endTempOpening();
return ResponseEntity.noContent().build(); return ResponseEntity.ok(newCode != null ? Map.of("newUnlockCode", newCode) : Map.of());
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Beenden der temporären Öffnung", e);
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
} }

View File

@@ -56,7 +56,9 @@ public class LockGameService implements LockControlCallback {
try { try {
benoetigtAktiv = new ObjectMapper().readValue(gamestate.getActiveTaskBenoetigtAktiv(), benoetigtAktiv = new ObjectMapper().readValue(gamestate.getActiveTaskBenoetigtAktiv(),
new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {}); new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {});
} catch (Exception ignored) {} } catch (Exception e) {
LOGGER.warn("Fehler beim Deserialisieren von activeTaskBenoetigtAktiv", e);
}
} }
return new GameState(gamestate.getGameId(), gamestate.getUserId(), gamestate.getLevel(), return new GameState(gamestate.getGameId(), gamestate.getUserId(), gamestate.getLevel(),
gamestate.getActiveTask(), gamestate.getActiveTaskEnd(), benoetigtAktiv, gamestate.getActiveTask(), gamestate.getActiveTaskEnd(), benoetigtAktiv,
@@ -86,6 +88,9 @@ public class LockGameService implements LockControlCallback {
String result = null; String result = null;
if (gamestate.getLevel() <= 5) { if (gamestate.getLevel() <= 5) {
gamestate.setTaskInQueue(null);
gamestate.setLockInQueue(null);
int nextInt = new Random().nextInt(1, 100); int nextInt = new Random().nextInt(1, 100);
boolean isZeitstrafe = nextInt < 25; boolean isZeitstrafe = nextInt < 25;
LOGGER.info("[LockGame {}] Ziehung: Level={}, Würfel={}, Typ={}", gamestate.getUserId(), LOGGER.info("[LockGame {}] Ziehung: Level={}, Würfel={}, Typ={}", gamestate.getUserId(),
@@ -132,24 +137,52 @@ public class LockGameService implements LockControlCallback {
} }
private String findAufgabe() throws JsonProcessingException { private String findAufgabe() throws JsonProcessingException {
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList(); var locked = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
ObjectMapper mapper = new ObjectMapper();
// First pass: unplayed tasks
var level = gamestate.getLevel(); var level = gamestate.getLevel();
while (level > 0) { while (level > 0) {
final var levelcp = level; final var levelcp = level;
var aufgaben = aufgabenList.getAufgaben().stream() var candidates = aufgabenList.getAufgaben().stream()
.filter(aufgabe -> aufgabe.getLevel() == levelcp && aufgabe.getBenoetigtAktiv().stream().noneMatch(item -> list.contains(item))) .filter(a -> a.getLevel() == levelcp && a.getBenoetigtAktiv().stream().noneMatch(locked::contains))
.toList(); .toList();
if (!aufgaben.isEmpty()) { if (!candidates.isEmpty()) {
ObjectMapper mapper = new ObjectMapper(); var aufgabe = candidates.get(new Random().nextInt(candidates.size()));
var aufgabe = aufgaben.get(new Random().nextInt(aufgaben.size())); LOGGER.info("[LockGame {}] AUFGABE gezogen (neu): kurzText='{}', level={}, sekunden={}-{}",
LOGGER.info("[LockGame {}] AUFGABE gezogen: kurzText='{}', level={}, sekunden={}-{}",
gamestate.getUserId(), aufgabe.getKurzText(), aufgabe.getLevel(), gamestate.getUserId(), aufgabe.getKurzText(), aufgabe.getLevel(),
aufgabe.getSekundenVon(), aufgabe.getSekundenBis()); aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
aufgabenList.getAufgaben().remove(aufgabe);
if (aufgabenList.getUsedAufgaben() == null) aufgabenList.setUsedAufgaben(new ArrayList<>());
aufgabenList.getUsedAufgaben().add(aufgabe);
gamestate.setAufgaben(mapper.writeValueAsString(aufgabenList));
gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe)); gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe));
return aufgabe.getText(); return aufgabe.getText();
} }
level--; level--;
} }
// Second pass: fallback to already-played tasks
var used = aufgabenList.getUsedAufgaben();
if (used != null && !used.isEmpty()) {
level = gamestate.getLevel();
while (level > 0) {
final var levelcp = level;
var candidates = used.stream()
.filter(a -> a.getLevel() == levelcp && a.getBenoetigtAktiv().stream().noneMatch(locked::contains))
.toList();
if (!candidates.isEmpty()) {
var aufgabe = candidates.get(new Random().nextInt(candidates.size()));
LOGGER.info("[LockGame {}] AUFGABE gezogen (Wiederholung): kurzText='{}', level={}, sekunden={}-{}",
gamestate.getUserId(), aufgabe.getKurzText(), aufgabe.getLevel(),
aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe));
return aufgabe.getText();
}
level--;
}
}
return null; return null;
} }
@@ -182,8 +215,8 @@ public class LockGameService implements LockControlCallback {
} }
private Integer getLockTime(Sperre sperre) { private Integer getLockTime(Sperre sperre) {
int von = sperre.getMinutenVon() != null ? sperre.getMinutenVon() : 0; int von = sperre.getMinutenVon() != null ? sperre.getMinutenVon() * 60 : 0;
int bis = sperre.getMinutenBis() != null ? sperre.getMinutenBis() : von; int bis = sperre.getMinutenBis() != null ? sperre.getMinutenBis() * 60 : von;
int time = von < bis ? new Random().nextInt(von, bis) : von; int time = von < bis ? new Random().nextInt(von, bis) : von;
return (int) (time * gamestate.getZeitfaktorZeitstrafen()); return (int) (time * gamestate.getZeitfaktorZeitstrafen());
} }
@@ -221,45 +254,59 @@ public class LockGameService implements LockControlCallback {
entity.setLockFor(lock.getSperreFuer()); entity.setLockFor(lock.getSperreFuer());
entity.setReleaseText(lock.getReleaseText()); entity.setReleaseText(lock.getReleaseText());
entity.setTempUnlockRequired(lock.getTempUnlockRequired()); entity.setTempUnlockRequired(lock.getTempUnlockRequired());
int lockMinutes = getLockTime(lock); int lockSeconds = getLockTime(lock);
entity.setReleaseTime(LocalDateTime.now().plusMinutes(lockMinutes)); entity.setReleaseTime(LocalDateTime.now().plusSeconds(lockSeconds));
LOGGER.info("[LockGame {}] ZEITSTRAFE aktiv: kurzText='{}', berechnete Zeit={}min (Range: {}min-{}min), sperreFuer={}", LOGGER.info("[LockGame {}] ZEITSTRAFE aktiv: kurzText='{}', berechnete Zeit={}min (Range: {}min-{}min), sperreFuer={}",
gamestate.getUserId(), lock.getKurzText(), lockMinutes, gamestate.getUserId(), lock.getKurzText(), lockSeconds,
lock.getMinutenVon(), lock.getMinutenBis(), lock.getSperreFuer()); lock.getMinutenVon(), lock.getMinutenBis(), lock.getSperreFuer());
lockGameLockRepository.save(entity); lockGameLockRepository.save(entity);
} }
public List<String> checkLocks() { public List<Sperre> checkLocks() {
var result = new ArrayList<String>(); var result = new ArrayList<Sperre>();
for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) { for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) {
if (entity.getReleaseTime().isBefore(LocalDateTime.now())) { if (entity.getReleaseTime().isBefore(LocalDateTime.now())) {
result.add(entity.getReleaseText()); result.add(toSperre(entity));
if (Boolean.TRUE.equals(entity.getTempUnlockRequired())) { if (Boolean.TRUE.equals(entity.getTempUnlockRequired())) {
startTempOpening(); startTempOpening();
} }
lockGameLockRepository.delete(entity); lockGameLockRepository.delete(entity);
} }
} }
return result; return result;
} }
public List<String> releaseLocks() { public List<Sperre> releaseLocks() {
var result = new ArrayList<String>(); var result = new ArrayList<Sperre>();
for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) { for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) {
result.add(entity.getReleaseText()); result.add(toSperre(entity));
lockGameLockRepository.delete(entity); lockGameLockRepository.delete(entity);
} }
return result; return result;
} }
private Sperre toSperre(LockGameLockEntity entity) {
var sperre = new Sperre();
sperre.setReleaseText(entity.getReleaseText());
sperre.setSperreFuer(entity.getLockFor());
sperre.setTempUnlockRequired(entity.getTempUnlockRequired());
return sperre;
}
public void initFinisher() throws JsonProcessingException { public void initFinisher() throws JsonProcessingException {
var finisher = aufgabenList.getFinisher().get(new Random().nextInt(aufgabenList.getFinisher().size())); var finisher = aufgabenList.getFinisher().get(new Random().nextInt(aufgabenList.getFinisher().size()));
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
gamestate.setFinisher(mapper.writeValueAsString(finisher)); gamestate.setFinisher(mapper.writeValueAsString(finisher));
gamestate.setActiveTask(null); gamestate.setActiveTask(null);
gamestate.setActiveTaskBenoetigtAktiv(null); gamestate.setActiveTaskBenoetigtAktiv(null);
lockGameLockRepository.deleteAll(lockGameLockRepository.findByGameId(gamestate.getGameId()));
var activeLocks = lockGameLockRepository.findByGameId(gamestate.getGameId());
boolean needsTempOpen = activeLocks.stream().anyMatch(l -> Boolean.TRUE.equals(l.getTempUnlockRequired()));
if (needsTempOpen) {
startTempOpening();
}
lockGameLockRepository.deleteAll(activeLocks);
lockGameRepository.save(gamestate); lockGameRepository.save(gamestate);
} }
@@ -293,7 +340,7 @@ public class LockGameService implements LockControlCallback {
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), TempOpeningReason.TASK.toString()); unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), TempOpeningReason.TASK.toString());
} }
public void endTempOpening() { public String endTempOpening() {
var lock = baseLockRepository.findById(gamestate.getLockId()).get(); var lock = baseLockRepository.findById(gamestate.getLockId()).get();
var overtime = BaseLockHelper.calcOvertime(lock); var overtime = BaseLockHelper.calcOvertime(lock);
if (overtime != null) { if (overtime != null) {
@@ -310,8 +357,13 @@ public class LockGameService implements LockControlCallback {
if (lockControl != null if (lockControl != null
&& lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) { && lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) {
lockControl.lock(); lockControl.lock();
return null;
} }
} }
var code = CodeCreator.createNumeric(lock.getUnlockCodeLength() != null ? lock.getUnlockCodeLength() : 5);
lock.setUnlockCode(code);
baseLockRepository.save(lock);
return code;
} }
@Override @Override

View File

@@ -9,6 +9,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@@ -45,6 +47,8 @@ import de.oaa.xxx.user.UserService;
@RequestMapping("/keyholder-offers") @RequestMapping("/keyholder-offers")
public class KeyholderOfferController { public class KeyholderOfferController {
private static final Logger LOGGER = LoggerFactory.getLogger(KeyholderOfferController.class);
private final KeyholderOfferRepository offerRepository; private final KeyholderOfferRepository offerRepository;
private final BaseLockTemplateRepository templateRepository; private final BaseLockTemplateRepository templateRepository;
private final TemplateSubscriptionRepository subscriptionRepository; private final TemplateSubscriptionRepository subscriptionRepository;
@@ -358,7 +362,9 @@ public class KeyholderOfferController {
try { try {
CardEnum card = CardEnum.valueOf(type); CardEnum card = CardEnum.valueOf(type);
for (int i = 0; i < count; i++) cards.add(card); for (int i = 0; i < count; i++) cards.add(card);
} catch (IllegalArgumentException ignored) {} } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Wert beim Keyholder-Angebot: {}", e.getMessage());
}
} }
}); });
return cards; return cards;

View File

@@ -1,39 +1,44 @@
package de.oaa.xxx.games.chastity.spinningwheel; package de.oaa.xxx.games.chastity.spinningwheel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import org.slf4j.Logger;
@Converter import org.slf4j.LoggerFactory;
public class SpinningWheelConverter implements AttributeConverter<List<SpinningWheelEntry>, String> {
@Converter
private static final ObjectMapper mapper = new ObjectMapper(); public class SpinningWheelConverter implements AttributeConverter<List<SpinningWheelEntry>, String> {
@Override private static final Logger LOGGER = LoggerFactory.getLogger(SpinningWheelConverter.class);
public String convertToDatabaseColumn(List<SpinningWheelEntry> list) { private static final ObjectMapper mapper = new ObjectMapper();
if (list == null || list.isEmpty())
return null; @Override
try { public String convertToDatabaseColumn(List<SpinningWheelEntry> list) {
return mapper.writeValueAsString(list); if (list == null || list.isEmpty())
} catch (Exception e) { return null;
return null; try {
} return mapper.writeValueAsString(list);
} } catch (Exception e) {
LOGGER.warn("Fehler beim Serialisieren der SpinningWheel-Einträge in DB-Spalte", e);
@Override return null;
public List<SpinningWheelEntry> convertToEntityAttribute(String json) { }
if (json == null || json.isBlank()) }
return new ArrayList<>();
try { @Override
return new ArrayList<>(mapper.readValue(json, new TypeReference<List<SpinningWheelEntry>>() { public List<SpinningWheelEntry> convertToEntityAttribute(String json) {
})); if (json == null || json.isBlank())
} catch (Exception e) { return new ArrayList<>();
return new ArrayList<>(); try {
} return new ArrayList<>(mapper.readValue(json, new TypeReference<List<SpinningWheelEntry>>() {
} }));
} } catch (Exception e) {
LOGGER.warn("Fehler beim Deserialisieren der SpinningWheel-Einträge aus DB-Spalte", e);
return new ArrayList<>();
}
}
}

View File

@@ -1,35 +1,40 @@
package de.oaa.xxx.games.chastity.tasks; package de.oaa.xxx.games.chastity.tasks;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import org.slf4j.Logger;
import java.util.ArrayList; import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.ArrayList;
@Converter import java.util.List;
public class TaskListConverter implements AttributeConverter<List<Task>, String> {
@Converter
private static final ObjectMapper mapper = new ObjectMapper(); public class TaskListConverter implements AttributeConverter<List<Task>, String> {
@Override private static final Logger LOGGER = LoggerFactory.getLogger(TaskListConverter.class);
public String convertToDatabaseColumn(List<Task> list) { private static final ObjectMapper mapper = new ObjectMapper();
if (list == null || list.isEmpty()) return null;
try { @Override
return mapper.writeValueAsString(list); public String convertToDatabaseColumn(List<Task> list) {
} catch (Exception e) { if (list == null || list.isEmpty()) return null;
return null; try {
} return mapper.writeValueAsString(list);
} } catch (Exception e) {
LOGGER.warn("Fehler beim Serialisieren der Task-Liste in DB-Spalte", e);
@Override return null;
public List<Task> convertToEntityAttribute(String json) { }
if (json == null || json.isBlank()) return new ArrayList<>(); }
try {
return new ArrayList<>(mapper.readValue(json, new TypeReference<List<Task>>() {})); @Override
} catch (Exception e) { public List<Task> convertToEntityAttribute(String json) {
return new ArrayList<>(); if (json == null || json.isBlank()) return new ArrayList<>();
} try {
} return new ArrayList<>(mapper.readValue(json, new TypeReference<List<Task>>() {}));
} } catch (Exception e) {
LOGGER.warn("Fehler beim Deserialisieren der Task-Liste aus DB-Spalte", e);
return new ArrayList<>();
}
}
}

View File

@@ -47,11 +47,15 @@ import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.social.SystemMessageService; import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService; import de.oaa.xxx.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController @RestController
@RequestMapping("/keyholder") @RequestMapping("/keyholder")
public class TimeLockController { public class TimeLockController {
private static final Logger LOGGER = LoggerFactory.getLogger(TimeLockController.class);
private final TimeLockRepository timeLockRepository; private final TimeLockRepository timeLockRepository;
private final TimeLockTemplateRepository templateRepository; private final TimeLockTemplateRepository templateRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
@@ -755,6 +759,7 @@ public class TimeLockController {
try { try {
until = LocalDateTime.parse(req.frozenUntil()); until = LocalDateTime.parse(req.frozenUntil());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Ungültiges Datumsformat für TimeLock-Freeze: {}", req.frozenUntil());
return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat.")); return ResponseEntity.badRequest().body(Map.of("error", "Ungültiges Datumsformat."));
} }
if (!until.isAfter(LocalDateTime.now())) if (!until.isAfter(LocalDateTime.now()))

View File

@@ -186,7 +186,7 @@ public class TTLockCallback {
wrapper.setRecords(recordList); wrapper.setRecords(recordList);
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("Fehler beim Parsen des TTLock Callbacks: " + e.getMessage()); LOGGER.error("Fehler beim Parsen des TTLock-Callbacks", e);
} }
return wrapper; return wrapper;

View File

@@ -20,9 +20,14 @@ import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service @Service
public class TTLockService { public class TTLockService {
private static final Logger LOGGER = LoggerFactory.getLogger(TTLockService.class);
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private static final String UNLOCK_CODE_NAME = "xxx-unlock-code"; private static final String UNLOCK_CODE_NAME = "xxx-unlock-code";
@@ -41,10 +46,10 @@ public class TTLockService {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
TTLockDetailResponse response = restTemplate.getForObject(url, TTLockDetailResponse.class); TTLockDetailResponse response = restTemplate.getForObject(url, TTLockDetailResponse.class);
System.out.println(response); LOGGER.debug("TTLock-Details abgerufen: {}", response);
return response; return response;
} catch (Exception e) { } catch (Exception e) {
System.err.println("Fehler beim Abrufen der Details: " + e.getMessage()); LOGGER.error("Fehler beim Abrufen der TTLock-Details für Lock {}", e.getMessage(), e);
return null; return null;
} }
} }
@@ -74,11 +79,11 @@ public class TTLockService {
if (response.getBody() != null && response.getBody().isSuccess()) { if (response.getBody() != null && response.getBody().isSuccess()) {
return response.getBody().getKeyboardPwdId(); return response.getBody().getKeyboardPwdId();
} else { } else {
System.out.println("Fehler von TTLock: " + response.getBody().getErrmsg()); LOGGER.warn("TTLock meldet Fehler beim Hinzufügen des Passcodes: {}", response.getBody().getErrmsg());
return null; return null;
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); LOGGER.error("Fehler beim Hinzufügen eines TTLock-Passcodes", e);
return null; return null;
} }
} }
@@ -103,6 +108,7 @@ public class TTLockService {
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class); ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
return response.getBody(); return response.getBody();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Fehler beim Löschen des TTLock-Passcodes", e);
return "{\"errcode\":-1, \"errmsg\":\"" + e.getMessage() + "\"}"; return "{\"errcode\":-1, \"errmsg\":\"" + e.getMessage() + "\"}";
} }
} }
@@ -134,7 +140,7 @@ public class TTLockService {
} }
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("Fehler beim Massenlöschen: " + e.getMessage()); LOGGER.error("Fehler beim Massenlöschen der TTLock-Passcodes", e);
} }
} }

View File

@@ -10,6 +10,7 @@ import java.util.List;
public class AufgabenList { public class AufgabenList {
private List<Aufgabe> aufgaben; private List<Aufgabe> aufgaben;
private List<Aufgabe> usedAufgaben;
private List<Sperre> sperren; private List<Sperre> sperren;
private List<Strafe> strafen; private List<Strafe> strafen;
private List<Finisher> finisher; private List<Finisher> finisher;

View File

@@ -11,6 +11,8 @@ import de.oaa.xxx.games.common.repository.SperreRepository;
import de.oaa.xxx.games.common.repository.StrafeRepository; import de.oaa.xxx.games.common.repository.StrafeRepository;
import de.oaa.xxx.games.common.repository.ToyRepository; import de.oaa.xxx.games.common.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -30,6 +32,8 @@ import java.util.UUID;
@Component @Component
public class DefaultFiller { public class DefaultFiller {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultFiller.class);
private final AufgabeRepository aufgabeRepository; private final AufgabeRepository aufgabeRepository;
private final AufgabenGruppeRepository gruppeRepository; private final AufgabenGruppeRepository gruppeRepository;
private final SperreRepository sperreRepository; private final SperreRepository sperreRepository;
@@ -362,7 +366,7 @@ public class DefaultFiller {
entity.setBild(stream.readAllBytes()); entity.setBild(stream.readAllBytes());
} }
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOGGER.error("Fehler beim Lesen des Gruppenbilds", e);
} }
gruppeRepository.save(entity); gruppeRepository.save(entity);
return entity; return entity;

View File

@@ -124,6 +124,7 @@ public class GruppenbeitragController extends BaseController {
try { try {
typ = BeitragTyp.valueOf(req.beitragTyp()); typ = BeitragTyp.valueOf(req.beitragTyp());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Ungültiger BeitragTyp: {}", req.beitragTyp());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }

View File

@@ -64,6 +64,7 @@ public class PasswordResetController {
try { try {
tokenId = UUID.fromString(confirm.token()); tokenId = UUID.fromString(confirm.token());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Parameter beim Passwort-Reset: {}", e.getMessage());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
var entity = passwordResetRepository.findById(tokenId); var entity = passwordResetRepository.findById(tokenId);

View File

@@ -2,6 +2,8 @@ package de.oaa.xxx.registration;
import java.net.URI; import java.net.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -12,6 +14,8 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/activation") @RequestMapping("/activation")
public class ActivationController { public class ActivationController {
private static final Logger LOGGER = LoggerFactory.getLogger(ActivationController.class);
private final RegistrationService registrationService; private final RegistrationService registrationService;
public ActivationController(RegistrationService registrationService) { public ActivationController(RegistrationService registrationService) {
@@ -25,9 +29,11 @@ public class ActivationController {
String redirect = "/login.html?email=" + java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8); String redirect = "/login.html?email=" + java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8);
return ResponseEntity.status(302).location(URI.create(redirect)).build(); return ResponseEntity.status(302).location(URI.create(redirect)).build();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
LOGGER.warn("Ungültiger Aktivierungsstatus: {}", e.getMessage());
// Bereits aktiviert → trotzdem zum Login weiterleiten (idempotent) // Bereits aktiviert → trotzdem zum Login weiterleiten (idempotent)
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Aktivierungsparameter: {}", e.getMessage());
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.internalServerError().build(); return ResponseEntity.internalServerError().build();

View File

@@ -39,6 +39,7 @@ public class RegistrationService {
registration = registrationRepository.findById(registrationId) registration = registrationRepository.findById(registrationId)
.orElseThrow(() -> new IllegalArgumentException("Registration nicht gefunden: " + token)); .orElseThrow(() -> new IllegalArgumentException("Registration nicht gefunden: " + token));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.debug("Aktivierungscode ist kein UUID-Format, suche nach Kurzcode: {}", token);
// Kein UUID-Format → nach kurzem Aktivierungscode suchen // Kein UUID-Format → nach kurzem Aktivierungscode suchen
registration = registrationRepository.findByActivationCode(token) registration = registrationRepository.findByActivationCode(token)
.orElseThrow(() -> new IllegalArgumentException("Aktivierungscode ungültig: " + token)); .orElseThrow(() -> new IllegalArgumentException("Aktivierungscode ungültig: " + token));

View File

@@ -1,6 +1,8 @@
package de.oaa.xxx.social; package de.oaa.xxx.social;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -15,6 +17,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Service @Service
public class SseService { public class SseService {
private static final Logger LOGGER = LoggerFactory.getLogger(SseService.class);
private final Map<UUID, List<SseEmitter>> emitters = new ConcurrentHashMap<>(); private final Map<UUID, List<SseEmitter>> emitters = new ConcurrentHashMap<>();
@PreDestroy @PreDestroy
@@ -42,6 +46,7 @@ public class SseService {
emitter.send(SseEmitter.event().name(eventName).data(data, MediaType.APPLICATION_JSON)); emitter.send(SseEmitter.event().name(eventName).data(data, MediaType.APPLICATION_JSON));
return false; return false;
} catch (IOException e) { } catch (IOException e) {
LOGGER.debug("SSE-Verbindung für User {} unterbrochen, Emitter wird entfernt", userId);
return true; return true;
} }
}); });

View File

@@ -89,7 +89,9 @@ public class LoginController {
if (jti != null) { if (jti != null) {
tokenBlacklist.blacklist(jti, claims.getExpiration().getTime()); tokenBlacklist.blacklist(jti, claims.getExpiration().getTime());
} }
} catch (Exception ignored) {} } catch (Exception e) {
LOGGER.debug("JWT beim Logout nicht valide, Blacklisting übersprungen", e);
}
}); });
} }
response.addHeader(HttpHeaders.SET_COOKIE, cookieFactory.jwtCookie("", Duration.ZERO).toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookieFactory.jwtCookie("", Duration.ZERO).toString());

View File

@@ -132,7 +132,7 @@ public class UserController {
user.setDatingLon(request.datingLon()); user.setDatingLon(request.datingLon());
if (request.datingGeschlechter() != null && !request.datingGeschlechter().isEmpty()) { if (request.datingGeschlechter() != null && !request.datingGeschlechter().isEmpty()) {
String joined = request.datingGeschlechter().stream() String joined = request.datingGeschlechter().stream()
.filter(g -> { try { Geschlecht.valueOf(g); return true; } catch (IllegalArgumentException e) { return false; } }) .filter(g -> { try { Geschlecht.valueOf(g); return true; } catch (IllegalArgumentException e) { LOGGER.warn("Ungültiger Wert in UserController: {}", g); return false; } })
.collect(Collectors.joining(",")); .collect(Collectors.joining(","));
user.setDatingGeschlechter(joined.isBlank() ? null : joined); user.setDatingGeschlechter(joined.isBlank() ? null : joined);
} else { } else {
@@ -154,7 +154,7 @@ public class UserController {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
if (request.datingGeschlechter() != null) { if (request.datingGeschlechter() != null) {
String joined = request.datingGeschlechter().stream() String joined = request.datingGeschlechter().stream()
.filter(g -> { try { Geschlecht.valueOf(g); return true; } catch (IllegalArgumentException e) { return false; } }) .filter(g -> { try { Geschlecht.valueOf(g); return true; } catch (IllegalArgumentException e) { LOGGER.warn("Ungültiger Wert in UserController: {}", g); return false; } })
.collect(Collectors.joining(",")); .collect(Collectors.joining(","));
user.setDatingGeschlechter(joined.isBlank() ? null : joined); user.setDatingGeschlechter(joined.isBlank() ? null : joined);
} }
@@ -248,6 +248,7 @@ public class UserController {
try { try {
cause = MessageCause.valueOf(entry.getKey()); cause = MessageCause.valueOf(entry.getKey());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Wert in UserController: {}", entry.getKey());
continue; continue;
} }
NotificationPreferenceEntity pref = notificationPreferenceRepository NotificationPreferenceEntity pref = notificationPreferenceRepository
@@ -500,8 +501,10 @@ public class UserController {
userService.createUser(registration); userService.createUser(registration);
return ResponseEntity.status(201).build(); return ResponseEntity.status(201).build();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Wert in UserController: {}", e.getMessage());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
LOGGER.warn("Ungültiger Wert in UserController: {}", e.getMessage());
return ResponseEntity.status(409).build(); return ResponseEntity.status(409).build();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e.getMessage(), e); LOGGER.error(e.getMessage(), e);

View File

@@ -3,6 +3,8 @@ package de.oaa.xxx.vorlieben;
import de.oaa.xxx.social.entity.FriendshipEntity; import de.oaa.xxx.social.entity.FriendshipEntity;
import de.oaa.xxx.social.repository.FriendshipRepository; import de.oaa.xxx.social.repository.FriendshipRepository;
import de.oaa.xxx.user.Sichtbarkeit; import de.oaa.xxx.user.Sichtbarkeit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import de.oaa.xxx.user.UserService; import de.oaa.xxx.user.UserService;
@@ -19,6 +21,8 @@ import java.util.stream.Collectors;
@Transactional @Transactional
public class VorliebenController { public class VorliebenController {
private static final Logger LOGGER = LoggerFactory.getLogger(VorliebenController.class);
private final VorliebeKategorieRepository kategorieRepository; private final VorliebeKategorieRepository kategorieRepository;
private final VorliebeItemRepository itemRepository; private final VorliebeItemRepository itemRepository;
private final UserVorliebeRepository userVorliebeRepository; private final UserVorliebeRepository userVorliebeRepository;
@@ -81,7 +85,10 @@ public class VorliebenController {
for (var entry : ratings.entrySet()) { for (var entry : ratings.entrySet()) {
UUID itemId; UUID itemId;
try { itemId = UUID.fromString(entry.getKey()); } try { itemId = UUID.fromString(entry.getKey()); }
catch (IllegalArgumentException e) { continue; } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Vorlieben-Wert: {}", entry.getKey());
continue;
}
String bewertungStr = entry.getValue(); String bewertungStr = entry.getValue();
if (bewertungStr == null || bewertungStr.isBlank()) { if (bewertungStr == null || bewertungStr.isBlank()) {
@@ -90,7 +97,10 @@ public class VorliebenController {
} else { } else {
VorliebeBewertung bewertung; VorliebeBewertung bewertung;
try { bewertung = VorliebeBewertung.valueOf(bewertungStr); } try { bewertung = VorliebeBewertung.valueOf(bewertungStr); }
catch (IllegalArgumentException e) { continue; } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Vorlieben-Wert: {}", bewertungStr);
continue;
}
UserVorliebeEntity uv = userVorliebeRepository.findByUserIdAndItemId(userId, itemId) UserVorliebeEntity uv = userVorliebeRepository.findByUserIdAndItemId(userId, itemId)
.orElseGet(() -> { .orElseGet(() -> {

View File

@@ -23,6 +23,7 @@
letter-spacing: 0.08em; letter-spacing: 0.08em;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
height: 1.2rem; height: 1.2rem;
text-align: center;
} }
.game-text { .game-text {
font-size: 1rem; font-size: 1rem;
@@ -31,6 +32,7 @@
white-space: pre-wrap; white-space: pre-wrap;
height: 14rem; height: 14rem;
overflow-y: auto; overflow-y: auto;
text-align: center;
} }
.game-timer { .game-timer {
font-size: 2.2rem; font-size: 2.2rem;
@@ -67,16 +69,6 @@
object-fit: contain; object-fit: contain;
} }
.lock-messages {
background: rgba(233,69,96,0.1);
border: 1px solid rgba(233,69,96,0.3);
border-radius: 10px;
padding: 1rem 1.1rem;
margin-bottom: 1.25rem;
}
.lock-messages p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; }
.lock-messages p:last-child { margin: 0; }
.init-box { .init-box {
background: var(--color-card); background: var(--color-card);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
@@ -137,15 +129,13 @@
margin-top: 0.6rem; margin-top: 0.6rem;
} }
#finisherBox { #levelTrophy {
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12)); font-size: 3.5rem;
border: 1px solid rgba(233,69,96,0.4); line-height: 72px;
border-radius: 14px; width: 72px;
padding: 1.5rem; height: 72px;
text-align: center; text-align: center;
} }
#finisherBox .trophy { font-size: 2.5rem; margin-bottom: 0.5rem; }
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
</style> </style>
</head> </head>
<body class="app"> <body class="app">
@@ -156,11 +146,9 @@
<!-- Level-Anzeige --> <!-- Level-Anzeige -->
<div class="level-display" id="levelDisplay" style="display:none;"> <div class="level-display" id="levelDisplay" style="display:none;">
<img id="levelImg" src="" alt="Level"> <img id="levelImg" src="" alt="Level">
<span id="levelTrophy" style="display:none;">🏆</span>
</div> </div>
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
<!-- Toy-Auswahl vor Spielstart --> <!-- Toy-Auswahl vor Spielstart -->
<div id="toyBox" style="display:none;"> <div id="toyBox" style="display:none;">
<div class="game-card"> <div class="game-card">
@@ -201,38 +189,50 @@
<!-- Release-Text (Sperren) --> <!-- Release-Text (Sperren) -->
<div id="lockReleaseBox" class="game-card" style="display:none;"> <div id="lockReleaseBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Sperre aufgehoben</div> <div class="game-label">🔓 Zeitstrafe verbüßt</div>
<div class="game-text" id="releaseText"></div> <div class="game-text" id="releaseText"></div>
<div style="margin-top:1.1rem;"> <div class="game-timer"></div>
<button class="btn-primary" id="btnReleaseOk">OK</button> <div class="game-btn-row">
<button class="btn-primary" id="btnReleaseOk" style="width:100%;height:100%;">OK</button>
</div> </div>
</div> </div>
<!-- Temporäre Öffnung --> <!-- Temporäre Öffnung -->
<div id="tempOpeningBox" class="game-card" style="display:none;"> <div id="tempOpeningBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Temporäre Öffnung erforderlich</div> <!-- Phase 1: Öffnung aktiv -->
<div class="game-text" id="tempOpeningTask"></div> <div id="tempPhase1">
<div id="tempOpeningCodeRow" style="display:none; margin-top:1rem; text-align:center;"> <div class="game-label" style="text-align:center;">🔓 Entsperrcode</div>
<div class="game-label">Entsperrcode</div> <div class="game-text" id="tempOpeningCode"
<div id="tempOpeningCode" style="font-size:1.8rem; font-weight:700; letter-spacing:0.18em; padding:0.6rem 0; color:var(--color-primary);"></div> style="display:flex;align-items:center;justify-content:center;
font-size:1.8rem;font-weight:700;letter-spacing:0.18em;
color:var(--color-primary);white-space:normal;"></div>
<div class="game-timer" id="tempOpeningTimer" style="margin-top:0.75rem;"></div>
<div class="game-btn-row">
<button class="btn-primary" style="width:100%;height:100%;" onclick="doEndTempOpening()">✓ Erledigt</button>
</div>
</div> </div>
<div class="game-btn-row"> <!-- Phase 2: Neuer Code zum Schließen -->
<button class="btn-primary" onclick="doEndTempOpening()">✓ Erledigt</button> <div id="tempPhase2" style="display:none;">
<div class="game-label" style="text-align:center;">🔒 Lock schließen</div>
<div class="game-text" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.75rem;white-space:normal;">
<span style="font-size:0.9rem;color:var(--color-muted);text-align:center;">Nutze den Entsperrcode um den Schlüssel wieder zu verschließen.</span>
<div id="tempNewCode" style="font-size:1.8rem;font-weight:700;letter-spacing:0.18em;color:var(--color-primary);text-align:center;"></div>
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;text-align:center;"></div>
</div>
<div class="game-timer"></div>
<div class="game-btn-row">
<button class="btn-primary" id="tempPhase2Btn" style="width:100%;height:100%;" onclick="startTempScramble()">OK</button>
</div>
</div> </div>
</div> </div>
<!-- Finisher --> <!-- Finisher -->
<div id="finisherBox" style="display:none;"> <div id="finisherBox" class="game-card" style="display:none;">
<div class="trophy">🏆</div> <div class="game-label" id="finisherLabel"></div>
<h2>Level 6 erreicht!</h2> <div class="game-text" id="finisherText"></div>
<div class="game-label" id="finisherTitle"></div> <div class="game-timer" id="finisherTimer"></div>
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div> <div class="game-btn-row">
<div id="finisherStart" style="margin-top:1.25rem;"> <button class="btn-primary" id="finisherBtn" style="width:100%;height:100%;"></button>
<button class="btn-primary" onclick="doStartFinisher()">▶ Starten</button>
</div>
<div id="finisherRunning" style="display:none;margin-top:1.25rem;">
<div class="game-timer active" id="finisherTimer">00:00</div>
<button class="btn-primary" onclick="doEndFinisher()" style="margin-top:1rem;">✓ Erledigt</button>
</div> </div>
</div> </div>
@@ -251,6 +251,17 @@
</div> </div>
</div> </div>
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:420px;">
<h2>Bestätigung</h2>
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Nein</button>
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Ja, abbrechen</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script> <script src="/js/icons.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script> <script>
@@ -262,6 +273,10 @@
let _state = null; let _state = null;
let _timerInt = null; let _timerInt = null;
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done' let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
let _tempOpeningTimerInt = null;
let _tempScrambleTimer = null;
let _tempScrambleCd = null;
let _tempOpeningFromQueue = false; // true only when opening was triggered from a queued Zeitstrafe (doQueueStart)
function goBack() { function goBack() {
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId; if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
@@ -376,6 +391,13 @@
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
hide('loadingHint'); hide('loadingHint');
// Remove ?fresh=1 from URL so F5 resumes instead of restarting.
const cleanParams = new URLSearchParams(location.search);
cleanParams.delete('fresh');
const cleanSearch = cleanParams.toString();
history.replaceState(null, '', location.pathname + (cleanSearch ? '?' + cleanSearch : ''));
await runGameLoop(); await runGameLoop();
} }
@@ -455,20 +477,26 @@
} }
async function runGameLoop() { async function runGameLoop() {
hide('gameCard');
hide('finisherBox');
hide('tempOpeningBox');
clearTimer(); clearTimer();
if (_state.tempOpeningTime) {
hide('finisherBox');
showTempOpeningDialog();
return;
}
if (_state.finisher) { if (_state.finisher) {
hide('tempOpeningBox');
showFinisherUI(); showFinisherUI();
return; return;
} }
if (_state.tempOpeningTime) { // Normal game states all use gameCard — show it once here so the card
showTempOpeningDialog(); // frame stays stable across transitions (queue ↔ active ↔ active-done).
return; show('gameBox');
} show('gameCard');
hide('tempOpeningBox');
hide('finisherBox');
renderLevelBar(_state.level); renderLevelBar(_state.level);
@@ -490,7 +518,8 @@
if (_state.lockInQueue) { if (_state.lockInQueue) {
let sperre; let sperre;
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; } try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten'); const btnLabel = sperre.tempUnlockRequired ? '⏱ Weiter zur temporären Öffnung' : '✓ Erledigt';
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', btnLabel);
} else if (_state.taskInQueue) { } else if (_state.taskInQueue) {
let aufgabe; let aufgabe;
@@ -500,13 +529,9 @@
hasDuration ? '▶ Starten' : '✓ Erledigt'); hasDuration ? '▶ Starten' : '✓ Erledigt');
} }
show('gameBox');
show('gameCard');
} }
function showActiveTask(text, endIso) { function showActiveTask(text, endIso) {
show('gameBox');
show('gameCard');
const timerEl = document.getElementById('gameTimer'); const timerEl = document.getElementById('gameTimer');
timerEl.classList.remove('active', 'urgent'); timerEl.classList.remove('active', 'urgent');
timerEl.textContent = ''; timerEl.textContent = '';
@@ -543,21 +568,21 @@
try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {} try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {}
} }
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten'); return; }
if (wasLock && tempUnlockRequired) { if (wasLock && tempUnlockRequired) {
await fetch('/lock-game/start-temp-opening', { method: 'POST' }); // Erst Öffnung starten, Zeitstrafe wird nach der Öffnung angewendet
const r = await fetch('/lock-game/start-temp-opening', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten der Öffnung'); return; }
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
_tempOpeningFromQueue = true;
showTempOpeningDialog(); showTempOpeningDialog();
} else if (wasLock) {
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
} else { } else {
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten'); return; }
if (wasLock) {
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
}
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
await runGameLoop(); await runGameLoop();
@@ -567,7 +592,13 @@
async function doQueueDone() { async function doQueueDone() {
try { try {
await checkAndShowLocks(); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' }); const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden'); return; } if (!applyR.ok) { showError('Fehler beim Anwenden'); return; }
const nextR = await fetch('/lock-game/next-task', { method: 'POST' }); const nextR = await fetch('/lock-game/next-task', { method: 'POST' });
@@ -582,9 +613,9 @@
clearTimer(); clearTimer();
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' }); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) { if (lockR.ok) {
const texts = await lockR.json(); const sperren = await lockR.json();
for (const text of (texts || [])) { for (const sperre of (sperren || [])) {
if (text != null && text !== '') await waitForReleaseOk(text); if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
} }
} }
_gameAction = 'active-done'; _gameAction = 'active-done';
@@ -595,9 +626,9 @@
try { try {
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' }); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) { if (lockR.ok) {
const texts = await lockR.json(); const sperren = await lockR.json();
for (const text of (texts || [])) { for (const sperre of (sperren || [])) {
if (text != null && text !== '') await waitForReleaseOk(text); if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
} }
} }
const r = await fetch('/lock-game/next-task', { method: 'POST' }); const r = await fetch('/lock-game/next-task', { method: 'POST' });
@@ -608,64 +639,149 @@
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); } } catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
} }
async function checkAndShowLocks() {
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
if (!r.ok) return;
const texts = await r.json();
const valid = texts ? texts.filter(t => t != null && t !== '') : [];
if (valid.length > 0) {
const box = document.getElementById('lockMessages');
box.innerHTML = valid.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
show('lockMessages');
await new Promise(res => setTimeout(res, 2000));
}
}
function showTempOpeningDialog() { function showTempOpeningDialog() {
show('gameBox'); show('gameBox');
hide('gameCard'); hide('gameCard');
hide('lockReleaseBox'); hide('lockReleaseBox');
hide('finisherBox'); hide('finisherBox');
document.getElementById('tempOpeningTask').textContent = _state.activeTask || ''; document.getElementById('tempOpeningCode').textContent = _state.tempOpeningCode || '';
const code = _state.tempOpeningCode;
if (code) { show('tempPhase1');
document.getElementById('tempOpeningCode').textContent = code; hide('tempPhase2');
show('tempOpeningCodeRow');
} else { const timerEl = document.getElementById('tempOpeningTimer');
hide('tempOpeningCodeRow'); timerEl.textContent = '';
timerEl.classList.remove('active', 'urgent');
if (_state.tempOpeningTime && _state.tempOpeningDuration) {
const endTime = new Date(new Date(_state.tempOpeningTime).getTime() + _state.tempOpeningDuration * 60000);
if (endTime > Date.now()) startTempOpeningTimer(endTime, timerEl);
} }
show('tempOpeningBox'); show('tempOpeningBox');
} }
function startTempOpeningTimer(endDate, el) {
if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
el.classList.add('active');
function tick() {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s;
el.classList.toggle('urgent', diff < 30);
if (diff === 0) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
}
tick();
_tempOpeningTimerInt = setInterval(tick, 1000);
}
async function doEndTempOpening() { async function doEndTempOpening() {
try { try {
await fetch('/lock-game/end-temp-opening', { method: 'POST' }); if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
await fetch('/lock-game/abandon-task', { method: 'POST' });
const stateR = await fetch('/lock-game/state'); // Only apply the queued Zeitstrafe when the opening was explicitly started from one (Path 1).
_state = await stateR.json(); // When triggered by a background lock expiry via checkLocks() (Path 2), lockInQueue may be set
hide('tempOpeningBox'); // to the *next* randomly-picked item — applying it would incorrectly create another background
await runGameLoop(); // lock with tempUnlockRequired and produce an infinite opening loop.
const fromQueue = _tempOpeningFromQueue;
_tempOpeningFromQueue = false;
const hasLockInQueue = fromQueue && !!_state.lockInQueue;
if (hasLockInQueue) {
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden der Zeitstrafe'); return; }
}
const endR = await fetch('/lock-game/end-temp-opening', { method: 'POST' });
if (!endR.ok) { showError('Fehler beim Beenden der Öffnung'); return; }
const endData = await endR.json();
const newCode = endData.newUnlockCode;
if (newCode) {
document.getElementById('tempNewCode').textContent = newCode;
document.getElementById('tempScrambleCountdown').style.display = 'none';
document.getElementById('tempPhase2Btn').textContent = 'OK';
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
hide('tempPhase1');
show('tempPhase2');
} else {
await finishTempOpening();
}
} catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); } } catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
} }
async function finishTempOpening() {
hide('tempOpeningBox');
await fetch('/lock-game/abandon-task', { method: 'POST' });
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
}
function startTempScramble() {
const codeEl = document.getElementById('tempNewCode');
const cdEl = document.getElementById('tempScrambleCountdown');
const btnEl = document.getElementById('tempPhase2Btn');
const len = codeEl.textContent.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
async function finish() {
stopped = true;
clearInterval(_tempScrambleTimer); _tempScrambleTimer = null;
clearInterval(_tempScrambleCd); _tempScrambleCd = null;
await finishTempOpening();
}
cdEl.style.display = '';
btnEl.textContent = 'Abbrechen';
btnEl.onclick = finish;
function updateCd() {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
cdEl.textContent = `${m}:${String(s).padStart(2, '0')}`;
}
updateCd();
_tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
_tempScrambleCd = setInterval(() => {
if (stopped) return;
if (--remaining <= 0) finish();
else updateCd();
}, 1000);
}
function showFinisherUI() { function showFinisherUI() {
show('gameBox'); show('gameBox');
hide('gameCard'); hide('gameCard');
hide('lockReleaseBox'); hide('lockReleaseBox');
hide('tempOpeningBox');
renderLevelBar(6);
let finisher = {}; let finisher = {};
try { finisher = JSON.parse(_state.finisher); } catch (_) {} try { finisher = JSON.parse(_state.finisher); } catch (_) {}
document.getElementById('finisherTitle').textContent = finisher.kurzText || ''; document.getElementById('finisherLabel').textContent = finisher.kurzText || '';
document.getElementById('finisherText').textContent = finisher.text || ''; document.getElementById('finisherText').textContent = finisher.text || '';
const timerEl = document.getElementById('finisherTimer');
const btnEl = document.getElementById('finisherBtn');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
if (_state.finisherStartedAt) { if (_state.finisherStartedAt) {
hide('finisherStart'); timerEl.classList.add('active');
show('finisherRunning');
startElapsedTimer(new Date(_state.finisherStartedAt)); startElapsedTimer(new Date(_state.finisherStartedAt));
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
} else { } else {
show('finisherStart'); btnEl.textContent = '▶ Starten';
hide('finisherRunning'); btnEl.onclick = doStartFinisher;
} }
show('finisherBox'); show('finisherBox');
} }
@@ -674,9 +790,12 @@
await fetch('/lock-game/start-finisher', { method: 'POST' }); await fetch('/lock-game/start-finisher', { method: 'POST' });
const r = await fetch('/lock-game/state'); const r = await fetch('/lock-game/state');
_state = await r.json(); _state = await r.json();
hide('finisherStart'); const timerEl = document.getElementById('finisherTimer');
show('finisherRunning'); timerEl.classList.add('active');
startElapsedTimer(new Date(_state.finisherStartedAt)); startElapsedTimer(new Date(_state.finisherStartedAt));
const btnEl = document.getElementById('finisherBtn');
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
} }
async function doEndFinisher() { async function doEndFinisher() {
@@ -690,12 +809,14 @@
function startElapsedTimer(startDate) { function startElapsedTimer(startDate) {
clearTimer(); clearTimer();
const el = document.getElementById('finisherTimer'); const el = document.getElementById('finisherTimer');
_timerInt = setInterval(() => { function tick() {
const diff = Math.floor((Date.now() - startDate) / 1000); const diff = Math.floor((Date.now() - startDate) / 1000);
const m = String(Math.floor(diff / 60)).padStart(2, '0'); const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0'); const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s; el.textContent = m + ':' + s;
}, 1000); }
tick();
_timerInt = setInterval(tick, 1000);
} }
function waitForReleaseOk(text) { function waitForReleaseOk(text) {
@@ -713,8 +834,12 @@
// ── Level-Bar ───────────────────────────────────────────────────────────── // ── Level-Bar ─────────────────────────────────────────────────────────────
function renderLevelBar(level) { function renderLevelBar(level) {
const lvl = Math.min(Math.max(level, 1), 5); const isFinisher = level >= 6;
document.getElementById('levelImg').src = `/img/lvl${lvl}.png`; document.getElementById('levelImg').style.display = isFinisher ? 'none' : '';
document.getElementById('levelTrophy').style.display = isFinisher ? '' : 'none';
if (!isFinisher) {
document.getElementById('levelImg').src = `/img/lvl${Math.min(Math.max(level, 1), 5)}.png`;
}
show('levelDisplay'); show('levelDisplay');
} }
@@ -727,7 +852,7 @@
function startTimer(endDate, el) { function startTimer(endDate, el) {
el.classList.add('active'); el.classList.add('active');
clearTimer(); clearTimer();
_timerInt = setInterval(() => { function tick() {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000)); const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
const m = String(Math.floor(diff / 60)).padStart(2, '0'); const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0'); const s = String(diff % 60).padStart(2, '0');
@@ -739,7 +864,9 @@
_gameAction = 'active-done'; _gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt'; document.getElementById('gameBtn').textContent = '✓ Erledigt';
} }
}, 1000); }
tick();
_timerInt = setInterval(tick, 1000);
} }
function clearTimer() { function clearTimer() {
@@ -779,16 +906,5 @@
boot(); boot();
</script> </script>
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:420px;">
<h2>Bestätigung</h2>
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Nein</button>
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Ja, abbrechen</button>
</div>
</div>
</div>
</body> </body>
</html> </html>