Files
xxx-sphere-web/bin/main/static/games/chastity/activelock.html
Mario 2b0ce62d33
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Menp überarbeitet
2026-04-08 16:52:43 +02:00

1750 lines
76 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chastity Game xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.keyholder-pending-banner {
display: flex;
align-items: flex-start;
gap: 0.75rem;
background: rgba(233,69,96,0.12);
border: 1px solid rgba(233,69,96,0.35);
border-radius: 10px;
padding: 1rem 1.1rem;
margin-bottom: 1.25rem;
font-size: 0.9rem;
color: var(--color-text);
line-height: 1.5;
}
.keyholder-pending-banner .icon { font-size: 1.4rem; flex-shrink: 0; }
/* ── Karten-Anzeige ── */
.cards-panel {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1rem 1.1rem;
margin-bottom: 1.25rem;
}
.cards-panel-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 0.75rem;
}
/* ── Nächste Karte Panel ── */
.nextcard-panel {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1rem 1.1rem;
margin-bottom: 1.25rem;
}
.nextcard-panel-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 0.75rem;
}
.nextcard-cards {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
position: relative;
border-radius: 6px;
padding: 0.75rem 0.5rem 0.5rem;
overflow: visible;
}
.nextcard-card-img {
width: calc((100% - 5 * 0.6rem) / 6);
height: auto;
border-radius: 6px;
position: relative;
z-index: 1;
transition: transform 0.15s, box-shadow 0.15s;
}
.nextcard-panel.drawable .nextcard-card-img:hover {
transform: translateY(-8px) scale(1.06);
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
z-index: 10;
cursor: pointer;
}
.nextcard-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.58);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.3rem;
z-index: 5;
border-radius: 6px;
}
.nextcard-timer-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
background: rgba(0,0,0,0.45);
border: 1px solid rgba(255,255,255,0.35);
border-radius: 10px;
padding: 0.55rem 1.1rem;
}
.nextcard-overlay-label {
font-size: 0.72rem;
font-weight: 700;
color: rgba(255,255,255,0.65);
text-transform: uppercase;
letter-spacing: 0.07em;
}
.nextcard-countdown {
font-size: 1.5rem;
font-weight: 700;
font-family: monospace;
color: #fff;
}
/* ── Frost-Overlay (Eis) gemeinsam für frozen + task ── */
.nextcard-overlay.frozen,
.nextcard-overlay.task {
border-radius: 6px;
background:
radial-gradient(ellipse at 18% 28%, rgba(255,255,255,0.38) 0%, transparent 48%),
radial-gradient(ellipse at 82% 72%, rgba(255,255,255,0.28) 0%, transparent 42%),
radial-gradient(ellipse at 55% 12%, rgba(255,255,255,0.22) 0%, transparent 38%),
rgba(55,140,210,0.68);
background-size: auto, auto, auto, 48px 48px;
}
/* ── Task-Overlay ── */
.nextcard-task-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 0.9rem 1.2rem;
max-width: 85%;
text-align: center;
}
.nextcard-task-text {
font-size: 0.92rem;
color: var(--color-text);
line-height: 1.5;
}
.nextcard-task-countdown {
font-size: 1.1rem;
font-weight: 700;
font-family: monospace;
color: var(--color-primary);
}
.btn-task-erledigt {
padding: 0.45rem 1.2rem;
border-radius: 8px;
border: none;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
background: #27ae60;
color: #fff;
transition: background 0.15s, opacity 0.15s;
width: auto;
}
.btn-task-erledigt:disabled {
background: rgba(255,255,255,0.2);
color: rgba(255,255,255,0.45);
cursor: not-allowed;
}
/* ── Hygiene-Panel ── */
.hygiene-panel {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1rem 1.1rem;
margin-bottom: 1.25rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.hygiene-panel-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 0.25rem;
}
.hygiene-countdown {
font-size: 1.4rem;
font-weight: 700;
font-family: monospace;
color: var(--color-text);
}
.btn-hygiene {
white-space: nowrap;
width: auto;
padding: 0.5rem 1.1rem;
font-size: 0.9rem;
transition: opacity 0.2s, background 0.2s;
}
.btn-hygiene:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
/* ── Verifikations-Panel ── */
.verification-panel {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1rem 1.1rem;
margin-bottom: 1.25rem;
}
.verification-panel-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 0.5rem;
}
.verification-panel-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.verification-due-text {
font-size: 0.95rem;
color: var(--color-text);
}
.btn-verification {
white-space: nowrap;
width: auto;
padding: 0.5rem 1.1rem;
font-size: 0.9rem;
}
.verification-code-label {
font-size: 0.85rem;
color: var(--color-muted);
margin-bottom: 0.35rem;
}
.verification-code-display {
font-size: 2rem;
font-weight: 700;
font-family: monospace;
letter-spacing: 0.25em;
color: var(--color-primary);
background: rgba(233,69,96,0.08);
border: 1px solid rgba(233,69,96,0.25);
border-radius: 8px;
padding: 0.5rem 1rem;
margin-bottom: 0.5rem;
text-align: center;
}
.verification-upload-label {
display: block;
cursor: pointer;
background: var(--color-secondary);
border: 1px dashed var(--color-muted);
border-radius: 8px;
padding: 0.75rem 1rem;
text-align: center;
font-size: 0.9rem;
color: var(--color-text);
transition: background 0.2s;
}
.verification-upload-label:hover { background: var(--color-hover); }
.verification-upload-label input[type="file"] { display: none; }
.verification-done-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.verification-done-check {
font-size: 0.95rem;
color: #2ecc71;
font-weight: 600;
}
.verification-done-check::before { content: '✓ '; }
.verification-votes {
display: flex;
gap: 1rem;
font-size: 0.9rem;
color: var(--color-muted);
}
.vote-up { color: #2ecc71; font-weight: 600; }
.vote-down { color: #e74c3c; font-weight: 600; }
/* ── Karte ziehen Dialog ── */
.draw-modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
z-index: 500;
align-items: center;
justify-content: center;
}
.draw-modal-backdrop.open { display: flex; }
.draw-modal-box {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 2rem 1.75rem 1.5rem;
max-width: 340px;
width: 90%;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
/* Flip-Karte */
.flip-card {
perspective: 900px;
width: 130px;
height: 195px;
}
.flip-card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.65s cubic-bezier(.4,0,.2,1);
transform-style: preserve-3d;
}
.flip-card-inner.flipped { transform: rotateY(180deg); }
.flip-card-front,
.flip-card-back {
position: absolute;
inset: 0;
backface-visibility: hidden;
}
.flip-card-front img,
.flip-card-back img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
.flip-card-back { transform: rotateY(180deg); }
/* Karten-Info unter der Karte */
.draw-card-info {
text-align: center;
opacity: 0;
transition: opacity 0.3s 0.2s;
}
.draw-card-info.visible { opacity: 1; }
.draw-card-info h3 { margin: 0 0 0.35rem; font-size: 1.05rem; }
.draw-card-info p { margin: 0; font-size: 0.85rem; color: var(--color-muted); line-height: 1.5; }
/* Grüne-Karte Entscheidung */
.draw-green-choice {
display: none;
flex-direction: column;
align-items: center;
gap: 0.6rem;
width: 100%;
}
.draw-green-choice.visible { display: flex; }
.draw-unlock-code {
font-size: 2rem;
font-weight: 700;
font-family: monospace;
letter-spacing: 0.15em;
color: var(--color-primary);
text-align: center;
display: none;
}
/* Buttons im Dialog */
.draw-modal-actions {
display: flex;
gap: 0.6rem;
justify-content: center;
width: 100%;
flex-wrap: wrap;
}
.draw-modal-actions button {
padding: 0.55rem 1.2rem;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
width: auto;
}
.btn-draw-ok {
background: var(--color-primary);
color: #fff;
}
.btn-draw-unlock {
background: #27ae60;
color: #fff;
}
.btn-draw-keep {
background: none;
border: 1px solid var(--color-secondary) !important;
color: var(--color-muted);
}
/* ── Hygiene-Öffnung Modal ── */
.hygiene-modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
z-index: 500;
align-items: center;
justify-content: center;
}
.hygiene-modal-backdrop.open { display: flex; }
.hygiene-modal-box {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 2rem 1.75rem 1.5rem;
max-width: 360px;
width: 90%;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
text-align: center;
}
.hygiene-modal-icon { font-size: 2.5rem; }
.hygiene-modal-box h3 { margin: 0; font-size: 1.05rem; }
.hygiene-unlock-code {
font-size: 2rem;
font-weight: 700;
font-family: monospace;
letter-spacing: 0.15em;
color: var(--color-primary);
background: var(--color-secondary);
border-radius: 8px;
padding: 0.5rem 1.2rem;
width: 100%;
box-sizing: border-box;
}
.hygiene-timer {
font-size: 1.6rem;
font-weight: 700;
font-family: monospace;
color: var(--color-text);
transition: color 0.3s;
}
.hygiene-timer.overtime { color: #e74c3c; }
.hygiene-timer-label {
font-size: 0.75rem;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: -0.5rem;
}
.hygiene-new-code-section { width: 100%; }
.hygiene-new-code-label {
font-size: 0.75rem;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 0.4rem;
}
/* ── Lock beenden Button ── */
.btn-lock-beenden {
background: transparent;
border: 1px solid rgba(200,50,50,0.45);
color: rgba(200,50,50,0.7);
font-size: 0.82rem;
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
margin-top: 1.5rem;
width: auto;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.btn-lock-beenden:hover {
background: rgba(200,50,50,0.12);
color: #e74c3c;
border-color: #e74c3c;
}
/* ── Warn-Modal ── */
.warn-modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
z-index: 500;
align-items: center;
justify-content: center;
}
.warn-modal-backdrop.open { display: flex; }
.warn-modal-box {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 12px;
padding: 2rem 1.75rem 1.5rem;
max-width: 360px;
width: 90%;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.warn-modal-box h3 { margin: 0; font-size: 1.05rem; }
.warn-modal-box p { margin: 0; font-size: 0.88rem; color: var(--color-muted); line-height: 1.55; }
.warn-modal-actions { display: flex; gap: 0.6rem; justify-content: flex-end; margin-top: 0.25rem; }
.warn-modal-actions .btn-cancel {
background: none;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.5rem 1.1rem;
border-radius: 7px;
cursor: pointer;
font-size: 0.88rem;
width: auto;
}
.warn-modal-actions .btn-danger {
background: #c0392b;
border: none;
color: #fff;
padding: 0.5rem 1.1rem;
border-radius: 7px;
cursor: pointer;
font-size: 0.88rem;
width: auto;
}
.warn-modal-actions .btn-danger:hover { background: #e74c3c; }
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h2 style="margin-bottom:1.25rem;">🔒 Chastity Session</h2>
<div id="keyholderInfoBar" style="display:none;align-items:center;gap:0.75rem;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:10px;padding:0.65rem 1rem;margin-bottom:1rem;">
<div id="keyholderInfoAvatar" style="width:42px;height:42px;border-radius:50%;background:var(--color-secondary);display:flex;align-items:center;justify-content:center;font-size:1.2rem;flex-shrink:0;overflow:hidden;border:1px solid rgba(255,255,255,0.08);"></div>
<div style="font-size:0.88rem;color:var(--color-muted);">Keyholder*In: <a id="keyholderInfoName" href="#" style="font-weight:700;color:var(--color-text);text-decoration:none;"></a></div>
</div>
<!-- Zugewiesene Aufgaben vom Keyholder -->
<div id="assignedTasksArea" style="display:none;margin-bottom:1rem;"></div>
<div class="keyholder-pending-banner" id="keyholderPendingBanner" style="display:none;">
<span class="icon"></span>
<div>
<strong>Keyholder*In-Bestätigung ausstehend</strong><br>
Die eingetragene Keyholder*In wurde per Direktnachricht eingeladen und muss die Rolle noch bestätigen.
Bis zur Bestätigung läuft das Lock als Self-Lock.
</div>
</div>
<!-- Karten-Panel (nur bei showRemainingCards) -->
<div class="cards-panel" id="cardsPanel" style="display:none;">
<div class="cards-panel-title">Verbleibende Karten</div>
<div id="cardsDisplay"></div>
</div>
<!-- Nächste Karte Panel -->
<div class="nextcard-panel" id="nextcardPanel" style="display:none;">
<div class="nextcard-panel-title">Nächste Karte</div>
<div class="nextcard-cards" id="nextcardCards">
<div class="nextcard-overlay" id="nextcardOverlay" style="display:none;">
<!-- Normaler Timer / Frozen -->
<div class="nextcard-timer-box" id="nextcardTimerBox">
<div class="nextcard-overlay-label" id="nextcardOverlayLabel">Nächste Karte ziehbar in</div>
<div class="nextcard-countdown" id="nextcardCountdown" style="display:none;"></div>
<div class="frozen-label" id="nextcardFrozenLabel" style="display:none;">❄️ Eingefroren</div>
</div>
<!-- Task -->
<div class="nextcard-task-box" id="nextcardTaskBox" style="display:none;">
<div class="nextcard-task-text" id="nextcardTaskText"></div>
<div id="nextcardTaskDescription" style="font-size:0.82rem;color:rgba(255,255,255,0.7);margin-top:0.25rem;line-height:1.4;display:none;"></div>
<div class="nextcard-task-countdown" id="nextcardTaskCountdown" style="display:none;"></div>
<button class="btn-task-erledigt" id="btnTaskErledigt" disabled onclick="taskErledigt()">✓ Erledigt</button>
</div>
</div>
</div>
</div>
<!-- Hygiene-Panel -->
<div class="hygiene-panel" id="hygienePanel" style="display:none;">
<div>
<div class="hygiene-panel-title">Nächste Hygiene-Öffnung</div>
<div class="hygiene-countdown" id="hygieneCountdown"></div>
</div>
<button class="btn-hygiene" id="hygieneBtn" style="display:none;" onclick="openHygieneModal()">🚿 Hygiene-Öffnung</button>
</div>
<!-- Verifikations-Panel -->
<div class="verification-panel" id="verificationPanel" style="display:none;">
<div class="verification-panel-title">Tägliche Verifikation</div>
<!-- Status: ausstehend (noch nicht gestartet) -->
<div class="verification-panel-body" id="verificationDueBody">
<span class="verification-due-text">Heute noch nicht verifiziert</span>
<button class="btn-verification" onclick="startVerification()">📷 Verifikation starten</button>
</div>
<!-- Status: gestartet, Bild fehlt noch -->
<div class="verification-panel-body" id="verificationPendingBody" style="display:none;">
<span class="verification-due-text">Verifikation gestartet Bild noch ausstehend</span>
<button class="btn-verification" onclick="startVerification()">📷 Fortsetzen</button>
</div>
<!-- Status: abgeschlossen -->
<div class="verification-done-body" id="verificationDoneBody" style="display:none;">
<div>
<div class="verification-done-check">Heute bereits verifiziert</div>
<div class="verification-votes" id="verificationVotes"></div>
</div>
<button class="btn-verification" id="btnVerificationRenew" style="display:none;" onclick="renewVerification()">🔄 Erneuern</button>
</div>
</div>
<div id="lockContent" style="color:var(--color-muted);font-size:0.95rem;">
Wird geladen…
</div>
<div id="lockActionArea" style="display:flex; justify-content:flex-end; margin-top:2rem;"></div>
</div>
</div>
<!-- Verifikations-Modal -->
<div class="hygiene-modal-backdrop" id="verificationModal" style="display:none;">
<div class="hygiene-modal-box">
<div class="hygiene-modal-icon">📷</div>
<div class="hygiene-modal-title">Tägliche Verifikation</div>
<div class="verification-code-label">Dein Verifikationscode:</div>
<div class="verification-code-display" id="verificationCode"></div>
<p style="font-size:0.88rem;color:var(--color-muted);margin:0.5rem 0 1rem;">
Fotografiere dich mit diesem Code gut sichtbar und lade das Bild hoch.
</p>
<label class="verification-upload-label">
<input type="file" id="verificationImageInput" accept="image/*" capture="user" onchange="onVerificationImageSelected(this)">
<span id="verificationUploadText">📁 Bild auswählen / aufnehmen</span>
</label>
<div id="verificationPreview" style="display:none;margin-top:0.75rem;">
<img id="verificationPreviewImg" style="max-width:100%;max-height:200px;border-radius:8px;object-fit:contain;">
</div>
<div style="display:flex;gap:0.75rem;margin-top:1.25rem;justify-content:flex-end;">
<button class="btn-modal-secondary" onclick="closeVerificationModal()">Abbrechen</button>
<button class="btn-modal-primary" id="verificationDoneBtn" disabled onclick="completeVerification()">✓ Fertig</button>
</div>
</div>
</div>
<!-- Hygiene-Öffnung-Modal -->
<div class="hygiene-modal-backdrop" id="hygieneModal">
<div class="hygiene-modal-box">
<div class="hygiene-modal-icon">🚿</div>
<h3>Hygiene-Öffnung</h3>
<!-- Phase 1: Öffnung läuft -->
<div id="hygienePhase1" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
<p style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein aktueller Entsperrcode:</p>
<div class="hygiene-unlock-code" id="hygieneCurrentCode"></div>
<div style="margin-top:0.5rem;display:flex;flex-direction:column;align-items:center;gap:0.2rem;">
<div class="hygiene-timer-label" id="hygieneTimerLabel">Verbleibende Zeit</div>
<div class="hygiene-timer" id="hygieneTimer"></div>
</div>
<button class="btn-draw-ok" style="margin-top:0.25rem;" onclick="endHygieneOpening()">✓ Beenden</button>
</div>
<!-- Phase 2: Neuer Code + Vergessen-Flow -->
<div id="hygienePhase2" style="display:none;width:100%;flex-direction:column;align-items:center;gap:0.75rem;">
<p id="hygienePhase2Hint" style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein neuer Code zum Abschließen:</p>
<div class="hygiene-unlock-code" id="hygieneNewCode"></div>
<div id="hygieneScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
<button class="btn-draw-ok" id="hygienePhase2Btn" onclick="startHygieneScramble()">OK</button>
</div>
</div>
</div>
<!-- Temp-Öffnung-Modal (CARD / TASK / etc.) -->
<div class="hygiene-modal-backdrop" id="tempOpeningModal">
<div class="hygiene-modal-box">
<div class="hygiene-modal-icon">🔓</div>
<h3>Lock geöffnet</h3>
<!-- Phase 1: Code anzeigen -->
<div id="tempPhase1" style="width:100%;display:flex;flex-direction:column;align-items:center;gap:0.75rem;">
<p style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein aktueller Entsperrcode:</p>
<div class="hygiene-unlock-code" id="tempOpeningCode"></div>
<button class="btn-draw-ok" style="margin-top:0.25rem;" onclick="endTempOpeningFlow()">🔒 Öffnung beenden</button>
</div>
<!-- Phase 2: Neuer Code (nur UNLOCK_CODE) -->
<div id="tempPhase2" style="display:none;width:100%;flex-direction:column;align-items:center;gap:0.75rem;">
<p id="tempPhase2Hint" style="margin:0;font-size:0.88rem;color:var(--color-muted);">Dein neuer Code zum Abschließen:</p>
<div class="hygiene-unlock-code" id="tempNewCode"></div>
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;"></div>
<button class="btn-draw-ok" id="tempPhase2Btn" onclick="startTempScramble()">OK</button>
</div>
</div>
</div>
<!-- Karte-Ziehen-Modal -->
<div class="draw-modal-backdrop" id="drawModal">
<div class="draw-modal-box">
<div class="flip-card">
<div class="flip-card-inner" id="flipCardInner">
<div class="flip-card-front">
<img src="/img/card.png" alt="Karte">
</div>
<div class="flip-card-back">
<img id="drawnCardImg" src="" alt="">
</div>
</div>
</div>
<div class="draw-card-info" id="drawCardInfo">
<h3 id="drawCardName"></h3>
<p id="drawCardDesc"></p>
</div>
<div id="drawTaskPendingHint" style="display:none;margin-top:0.75rem;padding:0.75rem 1rem;border-radius:8px;background:rgba(233,69,96,0.10);border:1px solid rgba(233,69,96,0.3);font-size:0.88rem;color:var(--color-text);line-height:1.5;text-align:center;">
<span id="drawTaskPendingText"></span>
</div>
<!-- Grüne Karte: Entscheidung -->
<div class="draw-green-choice" id="drawGreenChoice">
<p id="drawGreenText" style="text-align:center;font-size:0.88rem;color:var(--color-muted);margin:0;">
Du hast die grüne Karte gezogen!<br>Möchtest du den Entsperrcode erhalten und die Session beenden,<br>oder die Karte zurücklegen?
</p>
<div class="draw-unlock-code" id="drawUnlockCode"></div>
</div>
<div class="draw-modal-actions" id="drawModalActions" style="display:none;">
<!-- Non-green: OK -->
<button class="btn-draw-ok" id="btnDrawOk" onclick="closeDrawModal()">OK</button>
<!-- Green: zwei Optionen -->
<button class="btn-draw-unlock" id="btnDrawUnlock" style="display:none;" onclick="confirmUnlock()">🔓 Entsperren</button>
<button class="btn-draw-keep" id="btnDrawKeep" style="display:none;" onclick="keepGreenCard()">Zurücklegen</button>
</div>
</div>
</div>
<!-- TTLock Lade-Dialog -->
<div class="warn-modal-backdrop" id="ttlLoadingModal">
<div class="warn-modal-box" style="text-align:center;padding:2rem 1.5rem;">
<div style="font-size:2rem;margin-bottom:0.75rem;"></div>
<div style="font-weight:600;margin-bottom:0.4rem;">TTLock-Kommunikation läuft…</div>
<div style="font-size:0.85rem;color:var(--color-muted);">Bitte warten, der TTLock-Server wird kontaktiert.</div>
</div>
</div>
<!-- Warn-Modal (TestLock beenden) -->
<div class="warn-modal-backdrop" id="warnModal">
<div class="warn-modal-box">
<h3>Lock beenden?</h3>
<p>Dein Entsperrcode:</p>
<div id="warnModalUnlockCode" style="font-size:1.6rem;font-weight:700;letter-spacing:0.15em;text-align:center;margin:0.5rem 0 1rem;color:var(--color-primary);"></div>
<div class="warn-modal-actions">
<button class="btn-cancel" onclick="closeWarnModal()">Abbrechen</button>
<button class="btn-danger" onclick="lockLoeschen()">Lock beenden</button>
</div>
</div>
</div>
<!-- Notfall-Modal -->
<div class="warn-modal-backdrop" id="emergencyModal">
<div class="warn-modal-box">
<h3>🆘 Notfall-Entsperrung</h3>
<div id="emergencyModalContent"></div>
<div class="warn-modal-actions" id="emergencyModalActions">
<button class="btn-cancel" onclick="closeEmergencyModal()">Abbrechen</button>
<button class="btn-danger" id="btnEmergencyConfirm" onclick="confirmEmergency()">Notfall bestätigen</button>
</div>
</div>
</div>
<!-- Annehmen/Ablehnen-Dialog für Keyholder-Aufgaben -->
<div id="assignedTaskModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:500;align-items:center;justify-content:center;">
<div style="background:var(--color-card);border:1px solid var(--color-secondary);border-radius:14px;padding:1.5rem;max-width:400px;width:92%;display:flex;flex-direction:column;gap:1rem;position:relative;">
<button onclick="closeAssignedTaskModal()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;"></button>
<h3 style="margin:0;font-size:1.05rem;">✅ Aufgabe gestellt</h3>
<div id="assignedTaskModalInfo" style="font-size:0.88rem;color:var(--color-muted);line-height:1.6;"></div>
<div id="assignedTaskPenaltyInfo" style="background:rgba(231,76,60,0.08);border:1px solid rgba(231,76,60,0.25);border-radius:8px;padding:0.65rem 0.85rem;font-size:0.83rem;color:#e74c3c;line-height:1.5;"></div>
<div id="assignedTaskModalError" style="display:none;font-size:0.85rem;color:#e74c3c;"></div>
<div style="display:flex;gap:0.6rem;justify-content:flex-end;">
<button onclick="respondAssignedTask('decline')" style="background:rgba(231,76,60,0.12);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">✕ Ablehnen</button>
<button onclick="respondAssignedTask('accept')" style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.4);color:#2ecc71;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;">✓ Annehmen</button>
</div>
</div>
</div>
<script src="/js/shared.js"></script>
<script src="/js/card-defs.js"></script>
<script src="/js/card-display.js"></script>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/social-sidebar.js"></script>
<script>
const params = new URLSearchParams(window.location.search);
const lockId = params.get('lockId');
fetch('/login/me').then(r => r.ok ? r.json() : null).then(user => {
if (!user) { window.location.href = '/login.html'; return; }
if (lockId) loadLock();
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
});
async function loadLock() {
const res = await fetch('/keyholder/cardlock/' + lockId);
if (res.status === 404) {
// Prüfen, ob es ein TimeLock ist
const tlRes = await fetch('/keyholder/timelock/' + lockId);
if (tlRes.ok) {
window.location.replace('/games/chastity/activetimelock.html?lockId=' + lockId);
return;
}
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
return;
}
if (!res.ok) {
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
return;
}
const lock = await res.json();
_currentLock = lock;
document.getElementById('lockContent').textContent = '';
document.getElementById('keyholderPendingBanner').style.display =
lock.keyholderInvitationPending ? '' : 'none';
renderKeyholderBar(lock);
renderAssignedTasks(lock);
renderNextCardPanel(lock);
renderHygienePanel(lock);
renderVerificationPanel(lock);
renderTempOpeningPanel(lock);
renderCardsPanel(lock);
if (lock.keyholderRequestedUnlock) {
showKeyholderUnlockModal(lock.unlockCode || '');
}
renderLockActionArea(lock);
}
function showKeyholderUnlockModal(unlockCode) {
const modal = document.getElementById('drawModal');
const inner = document.getElementById('flipCardInner');
const info = document.getElementById('drawCardInfo');
const green = document.getElementById('drawGreenChoice');
const actions = document.getElementById('drawModalActions');
const hint = document.getElementById('drawTaskPendingHint');
// Direkt aufgedeckt mit grüner Karte zeigen
inner.classList.remove('flipped');
info.classList.remove('visible');
green.classList.remove('visible');
hint.style.display = 'none';
document.getElementById('drawUnlockCode').style.display = 'none';
document.getElementById('btnDrawOk').style.display = 'none';
document.getElementById('btnDrawUnlock').style.display = 'none';
document.getElementById('btnDrawKeep').style.display = 'none';
actions.style.display = 'none';
drawnUnlockCode = unlockCode;
modal.classList.add('open');
const def = CARD_LABELS['GREEN'];
setTimeout(() => {
document.getElementById('drawnCardImg').src = def.img;
document.getElementById('drawnCardImg').alt = def.name;
inner.classList.add('flipped');
setTimeout(() => {
const khName = _currentLock && _currentLock.keyholderName ? _currentLock.keyholderName : null;
document.getElementById('drawCardName').textContent = def.name;
document.getElementById('drawCardDesc').textContent = khName
? `🔑 ${khName} hat das Lock freigegeben.`
: '🔓 Das Lock wird freigegeben.';
document.getElementById('drawGreenText').style.display = 'none';
info.classList.add('visible');
actions.style.display = '';
green.classList.add('visible');
document.getElementById('btnDrawUnlock').style.display = '';
document.getElementById('btnDrawKeep').style.display = 'none';
}, 700);
}, 800);
}
let _currentLock = null;
function renderLockActionArea(lock) {
const area = document.getElementById('lockActionArea');
if (!area) return;
if (lock.testLock) {
area.innerHTML = `<button class="btn-lock-beenden" onclick="lockBeendenFragen()">🔓 Lock beenden</button>`;
} else if (lock.emergencyUnlockRequested) {
const emergencyMsg = lock.keyholderName
? `⏳ Notfall-Entsperrung angefordert ${lock.keyholderName} wurde benachrichtigt.`
: `⏳ Notfall-Entsperrung angefordert Das Lock öffnet sich automatisch.`;
area.innerHTML = `<div style="font-size:0.85rem;color:var(--color-muted);padding:0.5rem 0.75rem;border:1px solid rgba(231,76,60,0.3);border-radius:8px;background:rgba(231,76,60,0.06);">
${emergencyMsg}
</div>`;
} else {
area.innerHTML = `<button onclick="openEmergencyModal()"
style="background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.35);color:#e74c3c;padding:0.4rem 0.9rem;border-radius:7px;cursor:pointer;font-size:0.82rem;font-weight:600;width:auto;">
🆘 Notfall
</button>`;
}
}
function openEmergencyModal() {
const hasKH = _currentLock && _currentLock.keyholderName;
const khName = hasKH ? _currentLock.keyholderName : null;
const text = hasKH
? `Im Notfall kannst du eine sofortige Freigabe anfordern.<br>
<strong style="color:var(--color-text);">${khName}</strong> wird benachrichtigt und hat <strong>1 Stunde</strong> Zeit zu reagieren.
Reagiert ${khName} nicht, öffnet sich das Lock automatisch.`
: `Im Notfall kannst du eine sofortige Freigabe anfordern.<br>
Da kein Keyholder zugewiesen ist, öffnet sich das Lock <strong>sofort</strong>.`;
document.getElementById('emergencyModalContent').innerHTML = `
<p style="font-size:0.88rem;color:var(--color-muted);line-height:1.5;margin:0 0 0.5rem;">${text}</p>`;
document.getElementById('emergencyModalActions').style.display = '';
document.getElementById('emergencyModal').classList.add('open');
}
function closeEmergencyModal() {
document.getElementById('emergencyModal').classList.remove('open');
}
async function confirmEmergency() {
document.getElementById('emergencyModalActions').style.display = 'none';
try {
const res = await fetch('/keyholder/cardlock/' + lockId + '/emergency-unlock', { method: 'POST' });
if (res.ok || res.status === 204) {
const hasKH2 = _currentLock && _currentLock.keyholderName;
const successText = hasKH2
? `✅ Notfall-Anfrage wurde gesendet. ${_currentLock.keyholderName} wurde benachrichtigt.`
: `✅ Notfall-Freigabe wurde ausgelöst. Das Lock öffnet sich jetzt.`;
document.getElementById('emergencyModalContent').innerHTML = `
<p style="font-size:0.88rem;color:#2ecc71;line-height:1.5;margin:0;">
${successText}
</p>`;
setTimeout(() => { closeEmergencyModal(); loadLock(); }, 2000);
}
} catch(e) {
document.getElementById('emergencyModalContent').innerHTML = `<p style="color:#e74c3c;">Fehler beim Senden der Anfrage.</p>`;
}
}
let tickInterval = null;
let hygienePanelTick = null;
function fmtCountdown(diffMs) {
const totalSec = Math.floor(diffMs / 1000);
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
return h > 0
? `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`
: `${m}:${String(s).padStart(2,'0')}`;
}
// ── Keyholder-Aufgaben ──────────────────────────────────────────────────────
let activeAssignedTaskId = null;
function fmtDateTime(iso) {
const dt = new Date(iso);
return dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
}
const assignedTaskCache = {};
function renderAssignedTasks(lock) {
const area = document.getElementById('assignedTasksArea');
const tasks = lock.assignedTasks || [];
// Aufgaben ausblenden wenn bereits eine Aufgabe aktiv ist (nicht bei Keyholder-Freeze)
if (tasks.length === 0 || (lock.currentTask && lock.currentTask.trim())) {
area.style.display = 'none'; area.innerHTML = ''; return;
}
area.style.display = '';
area.innerHTML = tasks.map(t => {
assignedTaskCache[t.taskId] = t;
const deadline = new Date(t.acceptDeadline);
const remaining = deadline - Date.now();
const urgent = remaining < 60 * 60 * 1000; // < 1h
const titleEsc = (t.taskTitle || '').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const descEsc = (t.taskDescription || '').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const mins = t.taskMinutes > 0 ? ` · ${t.taskMinutes} Min.` : '';
return `<div onclick="openAssignedTaskModal('${t.taskId}')"
style="background:var(--color-card);border:1px solid ${urgent ? 'rgba(231,76,60,0.5)' : 'var(--color-secondary)'};
border-radius:10px;padding:0.75rem 1rem;margin-bottom:0.5rem;cursor:pointer;display:flex;align-items:flex-start;gap:0.6rem;"
onmouseover="this.style.borderColor='var(--color-primary)'" onmouseout="this.style.borderColor='${urgent ? 'rgba(231,76,60,0.5)' : 'var(--color-secondary)'}'">
<span style="font-size:1.3rem;flex-shrink:0;line-height:1.4;">✅</span>
<div style="flex:1;min-width:0;">
<div style="font-weight:700;font-size:0.92rem;margin-bottom:0.15rem;">${titleEsc}${mins}</div>
${descEsc ? `<div style="font-size:0.82rem;color:var(--color-muted);margin-bottom:0.25rem;line-height:1.4;">${descEsc}</div>` : ''}
<div style="font-size:0.78rem;color:var(--color-muted);">Gestellt: ${fmtDateTime(t.assignedAt)}</div>
<div style="font-size:0.78rem;color:${urgent ? '#e74c3c' : 'var(--color-muted)'};">Fällig bis: ${fmtDateTime(t.acceptDeadline)}</div>
</div>
<span style="font-size:0.75rem;color:var(--color-primary);font-weight:600;flex-shrink:0;align-self:center;">Details →</span>
</div>`;
}).join('');
}
function openAssignedTaskModal(taskId) {
const t = assignedTaskCache[taskId];
if (!t) return;
activeAssignedTaskId = taskId;
document.getElementById('assignedTaskModalError').style.display = 'none';
const titleEsc = (t.taskTitle || '').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const descEsc = (t.taskDescription || '').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const taskMinutes = t.taskMinutes || 0;
const penaltyFreezeMinutes = t.penaltyFreezeMinutes;
const penaltyRedCards = t.penaltyRedCards;
const minsHtml = taskMinutes > 0 ? `<div>Zeit: <strong>${taskMinutes} Minute${taskMinutes !== 1 ? 'n' : ''}</strong></div>` : '';
const descHtml = descEsc ? `<div style="margin-top:0.35rem;">${descEsc}</div>` : '';
document.getElementById('assignedTaskModalInfo').innerHTML =
`<div style="font-weight:700;font-size:0.97rem;color:var(--color-text);margin-bottom:0.4rem;">${titleEsc}</div>` +
descHtml + minsHtml +
`<div style="margin-top:0.5rem;">Gestellt am: <strong>${fmtDateTime(t.assignedAt)}</strong></div>` +
`<div>Annehmen bis: <strong>${fmtDateTime(t.acceptDeadline)}</strong></div>`;
const penaltyParts = [];
if (penaltyFreezeMinutes > 0) {
const d = Math.floor(penaltyFreezeMinutes / 1440);
const h = Math.floor((penaltyFreezeMinutes % 1440) / 60);
const m = penaltyFreezeMinutes % 60;
const parts = [];
if (d) parts.push(d + 'd');
if (h) parts.push(h + 'h');
if (m) parts.push(m + 'min');
penaltyParts.push('❄️ Einfrieren für ' + parts.join(' '));
}
if (penaltyRedCards > 0) penaltyParts.push('🔴 ' + penaltyRedCards + ' rote Karte' + (penaltyRedCards !== 1 ? 'n' : '') + ' hinzufügen');
document.getElementById('assignedTaskPenaltyInfo').innerHTML =
'<strong>Strafe bei Ablehnung:</strong><br>' + (penaltyParts.join('<br>') || '');
document.getElementById('assignedTaskModal').style.display = 'flex';
}
function closeAssignedTaskModal() {
document.getElementById('assignedTaskModal').style.display = 'none';
activeAssignedTaskId = null;
}
async function respondAssignedTask(action) {
if (!activeAssignedTaskId) return;
const errEl = document.getElementById('assignedTaskModalError');
errEl.style.display = 'none';
try {
const res = await fetch(`/keyholder/cardlock/${lockId}/assigned-tasks/${activeAssignedTaskId}/${action}`, { method: 'POST' });
if (res.ok || res.status === 204) {
closeAssignedTaskModal();
loadLock();
} else {
const data = await res.json().catch(() => ({}));
errEl.textContent = data.error || 'Fehler.';
errEl.style.display = '';
}
} catch(e) {
errEl.textContent = 'Fehler bei der Verbindung.';
errEl.style.display = '';
}
}
document.getElementById('assignedTaskModal').addEventListener('click', e => {
if (e.target === e.currentTarget) closeAssignedTaskModal();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && document.getElementById('assignedTaskModal').style.display === 'flex')
closeAssignedTaskModal();
});
// ── Keyholder-Bar ──────────────────────────────────────────────────────────
function renderKeyholderBar(lock) {
const bar = document.getElementById('keyholderInfoBar');
const avatar = document.getElementById('keyholderInfoAvatar');
const name = document.getElementById('keyholderInfoName');
if (!lock.hasKeyholder || !lock.keyholderName) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
name.textContent = lock.keyholderName;
name.href = '/community/benutzer.html?userId=' + lock.keyholderUserId;
if (lock.keyholderProfilePic) {
avatar.innerHTML = `<img src="data:image/jpeg;base64,${lock.keyholderProfilePic}" alt="" style="width:100%;height:100%;object-fit:cover;">`;
} else {
avatar.textContent = '👤';
}
}
function renderNextCardPanel(lock) {
if (tickInterval) { clearInterval(tickInterval); tickInterval = null; }
const panel = document.getElementById('nextcardPanel');
const cardsDiv = document.getElementById('nextcardCards');
const overlay = document.getElementById('nextcardOverlay');
panel.style.display = '';
panel.classList.remove('drawable');
// Karten-Bilder rendern (Overlay bleibt erhalten)
cardsDiv.querySelectorAll('.nextcard-card-img').forEach(el => el.remove());
const total = lock.totalCards || 0;
const show = Math.min(total, 36);
for (let i = 0; i < show; i++) {
const img = document.createElement('img');
img.className = 'nextcard-card-img';
img.src = '/img/card.png';
img.alt = 'Karte';
cardsDiv.insertBefore(img, overlay);
}
// Overlay-Elemente
const timerBox = document.getElementById('nextcardTimerBox');
const taskBox = document.getElementById('nextcardTaskBox');
const countdown = document.getElementById('nextcardCountdown');
const frozenLabel = document.getElementById('nextcardFrozenLabel');
const overlayLabel = document.getElementById('nextcardOverlayLabel');
const taskText = document.getElementById('nextcardTaskText');
const taskDesc = document.getElementById('nextcardTaskDescription');
const taskCountdown = document.getElementById('nextcardTaskCountdown');
const btnErledigt = document.getElementById('btnTaskErledigt');
// Overlay-Reset
overlay.classList.remove('frozen', 'task');
timerBox.style.display = 'none';
taskBox.style.display = 'none';
countdown.style.display = 'none';
frozenLabel.style.display = 'none';
const frozenUntill = lock.frozenUntill ? new Date(lock.frozenUntill) : null;
const taskFrozenUntil = lock.taskFrozenUntil ? new Date(lock.taskFrozenUntil) : null;
const currentTask = lock.currentTask || null;
const currentTaskDesc = lock.currentTaskDescription || null;
// ── Zustand 1: Aktive Aufgabe (mit oder ohne Task-Timer) ─────────────
if (currentTask) {
overlay.style.display = '';
overlay.classList.add('task');
taskBox.style.display = '';
taskText.textContent = currentTask;
if (currentTaskDesc) {
taskDesc.textContent = currentTaskDesc;
taskDesc.style.display = '';
} else {
taskDesc.style.display = 'none';
}
if (taskFrozenUntil && taskFrozenUntil > new Date()) {
btnErledigt.disabled = true;
taskCountdown.style.display = '';
function tickTask() {
const diff = taskFrozenUntil - Date.now();
if (diff <= 0) {
taskCountdown.style.display = 'none';
btnErledigt.disabled = false;
clearInterval(tickInterval); tickInterval = null;
return;
}
taskCountdown.textContent = '⏱ noch ' + fmtCountdown(diff);
}
tickTask();
tickInterval = setInterval(tickTask, 1000);
} else {
btnErledigt.disabled = false;
taskCountdown.style.display = 'none';
}
return;
}
// ── Zustand 2: Nur Keyholder-Freeze (keine Aufgabe) ──────────────────
if (frozenUntill && frozenUntill > new Date()) {
overlay.style.display = '';
overlay.classList.add('frozen');
timerBox.style.display = '';
overlayLabel.style.display = 'none';
frozenLabel.style.display = '';
return;
}
// ── Normalzustand: ziehbar oder Countdown ───────────────────────────
overlayLabel.style.display = '';
overlayLabel.textContent = 'Nächste Karte ziehbar in';
countdown.style.display = '';
if (lock.openPicks > 0) {
overlay.style.display = 'none';
if (!lock.keyholderRequestedUnlock) {
panel.classList.add('drawable');
enableCardClick();
}
return;
}
if (!lock.nextCardIn) { overlay.style.display = 'none'; return; }
overlay.style.display = '';
timerBox.style.display = '';
const target = new Date(lock.nextCardIn);
function tick() {
const diff = target - Date.now();
if (diff <= 0) {
overlay.style.display = 'none';
if (!lock.keyholderRequestedUnlock) {
panel.classList.add('drawable');
enableCardClick();
}
clearInterval(tickInterval); tickInterval = null;
return;
}
countdown.textContent = fmtCountdown(diff);
}
tick();
tickInterval = setInterval(tick, 1000);
}
function renderHygienePanel(lock) {
if (hygienePanelTick) { clearInterval(hygienePanelTick); hygienePanelTick = null; }
if (!lock.hygieneEnabled) return;
const panel = document.getElementById('hygienePanel');
const countdown = document.getElementById('hygieneCountdown');
const btn = document.getElementById('hygieneBtn');
panel.style.display = '';
btn.style.display = '';
if (lock.hygieneOpeningDue) {
countdown.textContent = 'Verfügbar';
countdown.style.display = '';
btn.disabled = false;
} else {
btn.disabled = true;
countdown.style.display = '';
let totalSeconds = lock.hygieneSecondsRemaining;
function tick() {
if (totalSeconds <= 0) {
countdown.style.display = 'none';
btn.disabled = false;
clearInterval(hygienePanelTick); hygienePanelTick = null;
return;
}
countdown.textContent = fmtCountdown(totalSeconds * 1000);
totalSeconds--;
}
tick();
hygienePanelTick = setInterval(tick, 1000);
}
}
function renderVerificationPanel(lock) {
const panel = document.getElementById('verificationPanel');
const dueBody = document.getElementById('verificationDueBody');
const pendingBody = document.getElementById('verificationPendingBody');
const doneBody = document.getElementById('verificationDoneBody');
if (!lock.verificationRequired) {
panel.style.display = 'none';
return;
}
panel.style.display = '';
window._pendingVerificationId = lock.verificationPendingId || null;
window._pendingVerificationCode = lock.verificationPendingCode || null;
if (!lock.verificationDue) {
// Abgeschlossen
dueBody.style.display = 'none';
pendingBody.style.display = 'none';
doneBody.style.display = '';
const up = lock.verificationUpvotes || 0;
const down = lock.verificationDownvotes || 0;
document.getElementById('verificationVotes').innerHTML =
`<span class="vote-up">👍 ${up}</span><span class="vote-down">👎 ${down}</span>`;
document.getElementById('btnVerificationRenew').style.display = down > up ? '' : 'none';
} else if (lock.verificationPendingId) {
// Gestartet, Bild ausstehend
dueBody.style.display = 'none';
pendingBody.style.display = '';
doneBody.style.display = 'none';
} else {
// Noch nicht gestartet
dueBody.style.display = '';
pendingBody.style.display = 'none';
doneBody.style.display = 'none';
}
}
async function renewVerification() {
const btn = document.getElementById('btnVerificationRenew');
btn.disabled = true;
const res = await fetch('/keyholder/cardlock/' + lockId + '/verification/today', { method: 'DELETE' });
if (res.ok || res.status === 204) {
loadLock();
} else {
btn.disabled = false;
}
}
function renderCardsPanel(lock) {
const panel = document.getElementById('cardsPanel');
const display = document.getElementById('cardsDisplay');
if (!lock.showRemainingCards) {
panel.style.display = 'none';
return;
}
panel.style.display = '';
display.innerHTML = cardTypeGridHtml(lock.availableCardCounts || {});
}
// ── Aufgabe erledigen ──
async function taskErledigt() {
const btn = document.getElementById('btnTaskErledigt');
btn.disabled = true;
await fetch('/keyholder/cardlock/' + lockId + '/task/complete', { method: 'POST' });
loadLock();
}
// ── Karte ziehen ──
let drawnUnlockCode = null;
function enableCardClick() {
document.querySelectorAll('.nextcard-card-img').forEach(img => {
img.addEventListener('click', onCardClick, { once: true });
});
}
async function onCardClick() {
// Alle weiteren Klicks blockieren
document.querySelectorAll('.nextcard-card-img').forEach(img => {
img.removeEventListener('click', onCardClick);
img.style.pointerEvents = 'none';
});
openDrawModal();
}
function openDrawModal() {
const modal = document.getElementById('drawModal');
const inner = document.getElementById('flipCardInner');
const info = document.getElementById('drawCardInfo');
const green = document.getElementById('drawGreenChoice');
const actions = document.getElementById('drawModalActions');
// Reset
inner.classList.remove('flipped');
info.classList.remove('visible');
green.classList.remove('visible');
document.getElementById('drawGreenText').style.display = '';
document.getElementById('drawUnlockCode').style.display = 'none';
document.getElementById('drawTaskPendingHint').style.display = 'none';
document.getElementById('btnDrawOk').style.display = '';
document.getElementById('btnDrawUnlock').style.display = 'none';
document.getElementById('btnDrawKeep').style.display = 'none';
actions.style.display = 'none';
drawnUnlockCode = null;
modal.classList.add('open');
// Karte serverseitig ziehen
fetch('/keyholder/cardlock/' + lockId + '/draw', { method: 'POST' })
.then(r => r.ok ? r.json() : Promise.reject(r.status))
.then(dto => {
const def = CARD_LABELS[dto.card] || { name: dto.card, img: '/img/card.png', desc: '' };
drawnUnlockCode = dto.unlockCode || null;
// Nach 1 Sekunde umdrehen
setTimeout(() => {
document.getElementById('drawnCardImg').src = def.img;
document.getElementById('drawnCardImg').alt = def.name;
inner.classList.add('flipped');
// Nach Ende der Animation Info + Buttons einblenden
setTimeout(() => {
document.getElementById('drawCardName').textContent = def.name;
document.getElementById('drawCardDesc').textContent = def.desc;
info.classList.add('visible');
actions.style.display = '';
if (dto.taskPending) {
const msgs = {
'KEYHOLDER': '🔑 Dein Keyholder wählt für dich eine Aufgabe aus. Du wirst benachrichtigt, sobald eine Aufgabe zugewiesen wurde.',
'COMMUNITY': '🗳️ Die Community stimmt ab, welche Aufgabe du erhältst. Das Ergebnis steht in einer Stunde fest.',
'RANDOM': '🎲 Da es sich um ein Test-Lock handelt, wird nach einer Stunde automatisch eine zufällige Aufgabe ausgewählt.',
};
document.getElementById('drawTaskPendingText').textContent = msgs[dto.taskPending] || '';
document.getElementById('drawTaskPendingHint').style.display = '';
}
if (dto.card === 'GREEN') {
green.classList.add('visible');
document.getElementById('btnDrawOk').style.display = 'none';
document.getElementById('btnDrawUnlock').style.display = '';
document.getElementById('btnDrawKeep').style.display = '';
}
}, 700);
}, 1000);
})
.catch(() => {
closeDrawModal();
alert('Fehler beim Ziehen der Karte.');
});
}
function closeDrawModal() {
document.getElementById('drawModal').classList.remove('open');
loadLock();
}
function confirmUnlock() {
if (drawnUnlockCode) {
document.getElementById('drawUnlockCode').textContent = drawnUnlockCode;
document.getElementById('drawUnlockCode').style.display = 'block';
}
// Session beenden nach Bestätigung → Warn-Modal überspringen, direkt löschen
document.getElementById('btnDrawUnlock').style.display = 'none';
document.getElementById('btnDrawKeep').style.display = 'none';
document.getElementById('btnDrawOk').style.display = '';
document.getElementById('btnDrawOk').textContent = 'Session beenden';
document.getElementById('btnDrawOk').onclick = () => lockLoeschen();
}
async function keepGreenCard() {
await fetch('/keyholder/cardlock/' + lockId + '/green/keep', { method: 'POST' });
closeDrawModal();
}
// ── Hygiene-Öffnung ──
let hygieneTickInterval = null;
async function openHygieneModal() {
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/start', { method: 'POST' });
if (!res.ok) { alert('Fehler beim Starten der Hygiene-Öffnung.'); return; }
const data = await res.json();
document.getElementById('hygieneCurrentCode').textContent = data.unlockCode;
document.getElementById('hygienePhase1').style.display = '';
document.getElementById('hygienePhase2').style.display = 'none';
document.getElementById('hygieneModal').classList.add('open');
const durationSec = (data.durationMinutes || 30) * 60;
const deadline = Date.now() + durationSec * 1000;
const timerEl = document.getElementById('hygieneTimer');
const labelEl = document.getElementById('hygieneTimerLabel');
if (hygieneTickInterval) clearInterval(hygieneTickInterval);
hygieneTickInterval = setInterval(() => {
const diff = deadline - Date.now();
if (diff > 0) {
timerEl.classList.remove('overtime');
labelEl.textContent = 'Verbleibende Zeit';
timerEl.textContent = fmtCountdown(diff);
} else {
timerEl.classList.add('overtime');
labelEl.textContent = 'Überschreitung';
timerEl.textContent = '+' + fmtCountdown(-diff);
}
}, 1000);
}
async function endHygieneOpening() {
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK';
if (isTtlock) {
document.getElementById('hygieneModal').classList.remove('open');
document.getElementById('ttlLoadingModal').classList.add('open');
}
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
if (isTtlock) {
document.getElementById('ttlLoadingModal').classList.remove('open');
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
loadLock();
return;
}
if (!res.ok) { alert('Fehler beim Beenden der Hygiene-Öffnung.'); return; }
const data = await res.json();
document.getElementById('hygienePhase1').style.display = 'none';
const phase2 = document.getElementById('hygienePhase2');
phase2.style.display = 'flex';
document.getElementById('hygieneNewCode').textContent = data.newUnlockCode;
}
function closeHygieneModal() {
document.getElementById('hygieneModal').classList.remove('open');
if (hygieneTickInterval) { clearInterval(hygieneTickInterval); hygieneTickInterval = null; }
if (hygieneScrambleTimer) { clearInterval(hygieneScrambleTimer); hygieneScrambleTimer = null; }
if (hygieneScrambleCd) { clearInterval(hygieneScrambleCd); hygieneScrambleCd = null; }
loadLock();
}
let hygieneScrambleTimer = null;
let hygieneScrambleCd = null;
function startHygieneScramble() {
const codeEl = document.getElementById('hygieneNewCode');
const hintEl = document.getElementById('hygienePhase2Hint');
const cdEl = document.getElementById('hygieneScrambleCountdown');
const btnEl = document.getElementById('hygienePhase2Btn');
const realCode = codeEl.textContent;
const len = realCode.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
function finish() {
stopped = true;
clearInterval(hygieneScrambleTimer); hygieneScrambleTimer = null;
clearInterval(hygieneScrambleCd); hygieneScrambleCd = null;
closeHygieneModal();
}
hintEl.style.display = 'none';
cdEl.style.display = '';
document.querySelector('#hygieneModal h3').textContent = 'Nun vergessen wir den Code…';
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();
hygieneScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
hygieneScrambleCd = setInterval(() => {
if (stopped) return;
remaining--;
updateCd();
if (remaining <= 0) finish();
}, 1000);
}
// ── Temp-Öffnung (CARD / TASK / etc.) ──
function renderTempOpeningPanel(lock) {
if (lock.tempOpeningActive) {
document.getElementById('tempOpeningCode').textContent = lock.tempOpeningUnlockCode || '';
document.getElementById('tempPhase1').style.display = '';
document.getElementById('tempPhase2').style.display = 'none';
document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet';
document.getElementById('tempOpeningModal').classList.add('open');
} else {
document.getElementById('tempOpeningModal').classList.remove('open');
}
}
async function endTempOpeningFlow() {
const isTtlock = _currentLock && _currentLock.controllType === 'TTLOCK';
const isTrust = _currentLock && _currentLock.controllType === 'TRUST';
if (isTtlock) {
document.getElementById('tempOpeningModal').classList.remove('open');
document.getElementById('ttlLoadingModal').classList.add('open');
}
const res = await fetch('/keyholder/cardlock/' + lockId + '/hygiene/end', { method: 'POST' });
if (isTtlock) {
document.getElementById('ttlLoadingModal').classList.remove('open');
if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; }
loadLock();
return;
}
if (!res.ok) { alert('Fehler beim Beenden der Öffnung.'); return; }
const data = await res.json();
if (isTrust || !data.newUnlockCode) {
document.getElementById('tempOpeningModal').classList.remove('open');
loadLock();
return;
}
// UNLOCK_CODE: neuen Code anzeigen
document.getElementById('tempPhase1').style.display = 'none';
document.getElementById('tempPhase2').style.display = 'flex';
document.getElementById('tempNewCode').textContent = data.newUnlockCode;
document.getElementById('tempPhase2Hint').style.display = '';
document.getElementById('tempPhase2Btn').textContent = 'OK';
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
document.getElementById('tempScrambleCountdown').style.display = 'none';
document.querySelector('#tempOpeningModal h3').textContent = 'Lock geöffnet';
}
function closeTempOpeningModal() {
document.getElementById('tempOpeningModal').classList.remove('open');
if (tempScrambleTimer) { clearInterval(tempScrambleTimer); tempScrambleTimer = null; }
if (tempScrambleCd) { clearInterval(tempScrambleCd); tempScrambleCd = null; }
loadLock();
}
let tempScrambleTimer = null;
let tempScrambleCd = null;
function startTempScramble() {
const codeEl = document.getElementById('tempNewCode');
const hintEl = document.getElementById('tempPhase2Hint');
const cdEl = document.getElementById('tempScrambleCountdown');
const btnEl = document.getElementById('tempPhase2Btn');
const realCode = codeEl.textContent;
const len = realCode.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
function finish() {
stopped = true;
clearInterval(tempScrambleTimer); tempScrambleTimer = null;
clearInterval(tempScrambleCd); tempScrambleCd = null;
closeTempOpeningModal();
}
hintEl.style.display = 'none';
cdEl.style.display = '';
document.querySelector('#tempOpeningModal h3').textContent = 'Nun vergessen wir den Code…';
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;
remaining--;
updateCd();
if (remaining <= 0) finish();
}, 1000);
}
// ── Lock beenden ──
function lockBeendenFragen() {
document.getElementById('warnModalUnlockCode').textContent = _currentLock ? (_currentLock.unlockCode || '') : '';
document.getElementById('warnModal').classList.add('open');
}
function closeWarnModal() {
document.getElementById('warnModal').classList.remove('open');
}
async function lockLoeschen() {
closeWarnModal();
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
document.getElementById('ttlLoadingModal').classList.add('open');
}
try {
await fetch('/keyholder/cardlock/' + lockId, { method: 'DELETE' });
} catch (_) { /* ignorieren */ }
window.location.href = '/userhome.html';
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeWarnModal();
});
// ── Verifikation ──
let currentVerificationId = null;
async function startVerification() {
let verificationId, code;
if (window._pendingVerificationId) {
verificationId = window._pendingVerificationId;
code = window._pendingVerificationCode;
} else {
const res = await fetch('/keyholder/cardlock/' + lockId + '/verification/start', { method: 'POST' });
if (!res.ok) return;
const data = await res.json();
verificationId = data.verificationId;
code = data.code;
}
currentVerificationId = verificationId;
document.getElementById('verificationCode').textContent = code;
document.getElementById('verificationImageInput').value = '';
document.getElementById('verificationUploadText').textContent = '📁 Bild auswählen / aufnehmen';
document.getElementById('verificationPreview').style.display = 'none';
document.getElementById('verificationDoneBtn').disabled = true;
document.getElementById('verificationModal').style.display = 'flex';
}
let _verificationBlob = null;
function onVerificationImageSelected(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
document.getElementById('verificationUploadText').textContent = '✓ ' + file.name;
document.getElementById('verificationDoneBtn').disabled = true;
_verificationBlob = null;
const img = new Image();
const reader = new FileReader();
reader.onload = e => {
img.onload = () => {
const MAX = 1024;
let w = img.width, h = img.height;
if (w > MAX || h > MAX) {
const scale = MAX / Math.max(w, h);
w = Math.round(w * scale);
h = Math.round(h * scale);
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
canvas.toBlob(blob => {
_verificationBlob = blob;
document.getElementById('verificationPreviewImg').src = canvas.toDataURL('image/jpeg');
document.getElementById('verificationPreview').style.display = '';
document.getElementById('verificationDoneBtn').disabled = false;
}, 'image/jpeg', 0.9);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
async function completeVerification() {
const btn = document.getElementById('verificationDoneBtn');
btn.disabled = true;
const form = new FormData();
form.append('image', _verificationBlob || document.getElementById('verificationImageInput').files[0], 'verification.jpg');
const res = await fetch('/keyholder/cardlock/' + lockId + '/verification/' + currentVerificationId + '/complete', {
method: 'POST',
body: form
});
if (res.ok) {
document.getElementById('verificationModal').style.display = 'none';
loadLock();
} else {
btn.disabled = false;
}
}
function closeVerificationModal() {
document.getElementById('verificationModal').style.display = 'none';
}
// ── Auto-Refresh alle 60 Sekunden ──
setInterval(loadLock, 60_000);
</script>
</body>
</html>