Files
xxx-sphere-web/bin/main/static/games/chastity/activelock.html
Mario 0aa794600e
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Weiter am Ingame Chastity Game gearbeitet
2026-04-26 22:53:05 +02:00

2133 lines
94 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;
align-items: center;
gap: 0.5rem;
position: relative;
padding: 0.75rem 0 1rem;
}
.nc-window {
flex: 1;
overflow-x: hidden;
overflow-y: visible;
min-width: 0;
padding-top: 14px;
position: relative;
}
.nc-slide-wrapper {
display: flex;
will-change: transform;
}
.nextcard-card-img {
flex-shrink: 0;
width: 130px;
height: auto;
border-radius: 6px;
position: relative;
z-index: 1;
margin-right: var(--nc-gap, 6px);
transition: transform 0.15s, box-shadow 0.15s;
}
.nextcard-card-img:last-child { margin-right: 0; }
.nextcard-panel.drawable .nextcard-card-img:hover {
transform: translateY(-10px) scale(1.08);
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
z-index: 10;
cursor: pointer;
}
.nc-nav-btn {
flex-shrink: 0;
width: 48px;
background: var(--color-secondary);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 8px;
color: var(--color-text);
font-size: 0.72rem;
font-weight: 600;
padding: 0.5rem 0.2rem;
cursor: pointer;
text-align: center;
line-height: 1.4;
align-self: stretch;
display: none;
transition: background 0.15s;
}
.nc-nav-btn:hover { background: var(--color-primary); }
/* ── Touch-Karussell ── */
.nextcard-cards.carousel-mode {
overflow: hidden;
height: 210px;
padding: 1rem 0 1rem;
justify-content: center;
}
.carousel-card {
position: absolute;
width: 100px;
border-radius: 8px;
box-shadow: 2px 4px 14px rgba(0,0,0,0.55);
transition: transform 0.18s cubic-bezier(.4,0,.2,1), opacity 0.18s;
user-select: none;
-webkit-user-drag: none;
}
.carousel-card.pos-center { transform: translateX(0) scale(1.18); opacity: 1; z-index: 5; }
.carousel-card.pos-left { transform: translateX(-120px) scale(0.84); opacity: 0.72; z-index: 3; }
.carousel-card.pos-right { transform: translateX(120px) scale(0.84); opacity: 0.72; z-index: 3; }
.carousel-card.pos-far-left { transform: translateX(-210px) scale(0.68); opacity: 0.4; z-index: 1; }
.carousel-card.pos-far-right { transform: translateX(210px) scale(0.68); opacity: 0.4; z-index: 1; }
.carousel-card.pos-exit-left {
transform: translateX(-400px) scale(0.4) rotate(-15deg);
opacity: 0; z-index: 0;
transition: transform 0.32s ease-in, opacity 0.32s ease-in;
}
.carousel-card.pos-exit-right {
transform: translateX(400px) scale(0.4) rotate(15deg);
opacity: 0; z-index: 0;
transition: transform 0.32s ease-in, opacity 0.32s ease-in;
}
.nextcard-panel.drawable .carousel-card.pos-center { 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>
<!-- Speed-Effekt-Panel -->
<div id="speedPanel" style="display:none;background:var(--color-card);border:1px solid var(--color-secondary);border-radius:12px;padding:0.85rem 1.1rem;gap:0.35rem;">
<div style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--color-muted);" id="speedPanelTitle">Slow Motion aktiv</div>
<div style="font-size:0.9rem;font-weight:600;" id="speedPanelInfo"></div>
</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>
<!-- Speed-Karte: Zeitpunkt wählen -->
<div id="drawSpeedPicker" style="display:none;margin-top:0.75rem;padding:0.75rem 1rem;border-radius:8px;background:rgba(100,149,237,0.10);border:1px solid rgba(100,149,237,0.3);gap:0.6rem;text-align:center;">
<div style="font-size:0.88rem;color:var(--color-text);">Wähle den Zeitpunkt, bis zu dem der Effekt aktiv sein soll:</div>
<input type="datetime-local" id="drawSpeedUntilInput" style="background:var(--color-secondary);border:1px solid var(--color-secondary);border-radius:7px;padding:0.45rem 0.75rem;color:var(--color-text);font-size:0.9rem;width:100%;box-sizing:border-box;">
</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>
<!-- Speed-Karte: Bestätigen -->
<button class="btn-draw-ok" id="btnSpeedConfirm" style="display:none;" onclick="confirmSpeedCard()">✓ Bestätigen</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);
renderSpeedPanel(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-Darstellung aufbauen
if (ncResizeObs) { ncResizeObs.disconnect(); ncResizeObs = null; }
cardsDiv.querySelectorAll('.nc-nav-btn, .nc-window, .carousel-card').forEach(el => el.remove());
cardsDiv.classList.remove('carousel-mode');
const total = lock.totalCards || 0;
const isTouch = window.matchMedia('(hover: none) and (pointer: coarse)').matches;
if (isTouch) initCarousel(cardsDiv, overlay, total);
else initCardWindow(cardsDiv, overlay, total);
// 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;
// ── Touch-Karussell ──
const CAROUSEL_POS = ['pos-far-left', 'pos-left', 'pos-center', 'pos-right', 'pos-far-right'];
let carouselCards = [];
let carouselShifting = false;
let carouselTouchX = 0;
let carouselTouchT = 0;
let carouselAbort = null;
let carouselDrawable = false;
function initCarousel(cardsDiv, overlay, total) {
if (carouselAbort) carouselAbort.abort();
carouselAbort = new AbortController();
const signal = carouselAbort.signal;
carouselCards = [];
carouselShifting = false;
carouselDrawable = false;
cardsDiv.classList.add('carousel-mode');
for (let i = 0; i < 5; i++) {
const img = document.createElement('img');
img.className = 'carousel-card ' + CAROUSEL_POS[i];
img.src = '/img/card.png';
img.alt = 'Karte';
cardsDiv.insertBefore(img, overlay);
carouselCards.push(img);
}
cardsDiv.addEventListener('touchstart', e => {
carouselTouchX = e.touches[0].clientX;
carouselTouchT = Date.now();
}, { passive: true, signal });
cardsDiv.addEventListener('touchend', e => {
if (carouselShifting) return;
const touch = e.changedTouches[0];
const dx = touch.clientX - carouselTouchX;
const dt = Date.now() - carouselTouchT;
const absDx = Math.abs(dx);
if (absDx < 5) {
// Tap: Element unter dem Finger bestimmen
const el = document.elementFromPoint(touch.clientX, touch.clientY);
const idx = el ? carouselCards.indexOf(el) : -1;
if (idx < 0) return;
if (idx === 2 && carouselDrawable) {
// Mittlere Karte → Karte ziehen
e.preventDefault();
carouselDrawable = false;
carouselCards.forEach(c => c.style.pointerEvents = 'none');
openDrawModal();
} else if (idx < 2) {
_doCarouselShift('right', 1);
} else {
_doCarouselShift('left', 1);
}
return;
}
if (!carouselDrawable) return;
// Swipe erkannt: Geschwindigkeit bestimmt wie viele Karten wechseln
const velocity = absDx / dt;
const steps = velocity > 1.2 ? 3 : velocity > 0.6 ? 2 : 1;
dx < 0 ? _doCarouselShift('left', steps) : _doCarouselShift('right', steps);
}, { passive: false, signal });
}
function shiftCarouselLeft(steps = 1) { _doCarouselShift('left', steps); }
function shiftCarouselRight(steps = 1) { _doCarouselShift('right', steps); }
function _doCarouselShift(dir, remaining) {
if (carouselShifting && remaining === /* first call */ remaining) {/* skip guard for chained */}
carouselShifting = true;
if (dir === 'left') {
carouselCards[0].className = 'carousel-card pos-exit-left';
for (let i = 1; i < 5; i++) carouselCards[i].className = 'carousel-card ' + CAROUSEL_POS[i - 1];
} else {
carouselCards[4].className = 'carousel-card pos-exit-right';
for (let i = 3; i >= 0; i--) carouselCards[i].className = 'carousel-card ' + CAROUSEL_POS[i + 1];
}
setTimeout(() => {
if (dir === 'left') {
const ex = carouselCards.shift();
ex.style.transition = 'none';
ex.className = 'carousel-card pos-exit-right';
ex.getBoundingClientRect();
ex.style.transition = '';
ex.className = 'carousel-card pos-far-right';
carouselCards.push(ex);
} else {
const ex = carouselCards.pop();
ex.style.transition = 'none';
ex.className = 'carousel-card pos-exit-left';
ex.getBoundingClientRect();
ex.style.transition = '';
ex.className = 'carousel-card pos-far-left';
carouselCards.unshift(ex);
}
if (remaining > 1) _doCarouselShift(dir, remaining - 1);
else carouselShifting = false;
}, 180);
}
// ── Karten-Fenster ──
let ncTotal = 0;
let ncWindowStart = 0;
let ncVisibleCount = 0;
let ncDrawable = false;
let ncWindowEl = null;
let ncBtnLeft = null;
let ncBtnRight = null;
let ncResizeObs = null;
function initCardWindow(cardsDiv, overlay, total) {
ncTotal = total;
ncDrawable = false;
ncWindowStart = 0;
if (total === 0) return;
ncBtnLeft = document.createElement('button');
ncBtnRight = document.createElement('button');
ncWindowEl = document.createElement('div');
const wrapper = document.createElement('div');
ncBtnLeft.className = 'nc-nav-btn';
ncBtnRight.className = 'nc-nav-btn';
ncWindowEl.className = 'nc-window';
wrapper.className = 'nc-slide-wrapper';
ncBtnLeft.addEventListener('click', () => scrollCardWindow(-1));
ncBtnRight.addEventListener('click', () => scrollCardWindow(1));
ncWindowEl.appendChild(wrapper);
cardsDiv.insertBefore(ncBtnLeft, overlay);
cardsDiv.insertBefore(ncWindowEl, overlay);
cardsDiv.insertBefore(ncBtnRight, overlay);
// Klick auf beliebige Karte → Karte ziehen (per Delegation)
ncWindowEl.addEventListener('click', e => {
if (!ncDrawable) return;
if (e.target.classList.contains('nextcard-card-img')) {
ncDrawable = false;
ncWindowEl.style.pointerEvents = 'none';
openDrawModal();
}
});
if (ncResizeObs) ncResizeObs.disconnect();
ncResizeObs = new ResizeObserver(() => recalcCardWindow());
ncResizeObs.observe(ncWindowEl);
requestAnimationFrame(() => recalcCardWindow(true));
}
function recalcCardWindow(initialCenter = false) {
const slots = Math.round(ncWindowEl.offsetWidth / 50);
const newCount = Math.min(ncTotal, Math.min(20, Math.max(3, slots)));
if (newCount === ncVisibleCount && !initialCenter) {
// Breite hat sich nicht verändert genug → nur Gap neu berechnen
renderCardWindow(null);
return;
}
ncVisibleCount = newCount;
if (initialCenter) {
ncWindowStart = Math.max(0, Math.floor(ncTotal / 2) - Math.floor(ncVisibleCount / 2));
} else {
// Fensterstart so anpassen, dass die Mitte des sichtbaren Bereichs gleich bleibt
const mid = ncWindowStart + Math.floor(ncVisibleCount / 2);
ncWindowStart = Math.max(0, Math.min(ncTotal - ncVisibleCount, mid - Math.floor(ncVisibleCount / 2)));
}
renderCardWindow(null);
}
function renderCardWindow(slideDir) {
const wrapper = ncWindowEl.querySelector('.nc-slide-wrapper');
const cardW = 130;
// Karten neu befüllen
wrapper.innerHTML = '';
for (let i = 0; i < ncVisibleCount; i++) {
const img = document.createElement('img');
img.className = 'nextcard-card-img';
img.src = '/img/card.png';
img.alt = 'Karte';
wrapper.appendChild(img);
}
// Dynamischer Overlap: alle sichtbaren Karten füllen die Fensterbreite
const windowW = ncWindowEl.offsetWidth;
let gap = ncVisibleCount <= 1 ? 6 : (windowW - ncVisibleCount * cardW) / (ncVisibleCount - 1);
gap = Math.max(-105, Math.min(12, gap));
ncWindowEl.style.setProperty('--nc-gap', gap + 'px');
// Animation: alte Karten raus, neue rein
if (slideDir) {
const oldWrapper = wrapper.cloneNode(true);
oldWrapper.style.cssText = 'position:absolute;top:0;left:0;width:100%;pointer-events:none;';
ncWindowEl.appendChild(oldWrapper);
// Neue Karten von der Seite einblenden
wrapper.style.transition = 'none';
wrapper.style.transform = `translateX(${slideDir > 0 ? '100%' : '-100%'})`;
wrapper.getBoundingClientRect();
wrapper.style.transition = 'transform 0.28s ease';
wrapper.style.transform = 'translateX(0)';
// Alte Karten zur anderen Seite ausblenden
oldWrapper.style.transition = 'transform 0.28s ease';
oldWrapper.style.transform = `translateX(${slideDir > 0 ? '-100%' : '100%'})`;
setTimeout(() => oldWrapper.remove(), 300);
}
// Nav-Buttons aktualisieren
const leftCount = ncWindowStart;
const rightCount = ncTotal - ncWindowStart - ncVisibleCount;
ncBtnLeft.style.display = leftCount > 0 ? 'block' : 'none';
ncBtnRight.style.display = rightCount > 0 ? 'block' : 'none';
ncBtnLeft.textContent = `\n${leftCount}`;
ncBtnRight.textContent = `${rightCount}\n`;
ncWindowEl.style.pointerEvents = '';
}
function scrollCardWindow(dir) {
const step = Math.max(1, Math.floor(ncVisibleCount / 2));
if (dir < 0) ncWindowStart = Math.max(0, ncWindowStart - step);
else ncWindowStart = Math.min(ncTotal - ncVisibleCount, ncWindowStart + step);
renderCardWindow(dir);
}
function enableCardClick() {
if (carouselCards.length > 0) {
carouselDrawable = true;
} else if (ncWindowEl) {
ncDrawable = true;
}
}
async function onCardClick() {
carouselDrawable = false;
carouselCards.forEach(c => c.style.pointerEvents = 'none');
if (ncWindowEl) ncWindowEl.style.pointerEvents = 'none';
document.querySelectorAll('.nextcard-card-img').forEach(img => img.style.pointerEvents = 'none');
openDrawModal();
}
let _pendingSpeedMode = null;
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('drawSpeedPicker').style.display = 'none';
document.getElementById('btnDrawOk').style.display = '';
document.getElementById('btnSpeedConfirm').style.display = 'none';
document.getElementById('btnDrawUnlock').style.display = 'none';
document.getElementById('btnDrawKeep').style.display = 'none';
actions.style.display = 'none';
drawnUnlockCode = null;
_pendingSpeedMode = 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 = '';
}
if (dto.card === 'SLOWMO_CARD' || dto.card === 'SPEEDUP_CARD') {
_pendingSpeedMode = dto.card === 'SLOWMO_CARD' ? 'SLOWMO' : 'SPEEDUP';
const picker = document.getElementById('drawSpeedPicker');
const input = document.getElementById('drawSpeedUntilInput');
// Minimum: jetzt + 1 Stunde, Standardwert: jetzt + 24 Stunden
const minDate = new Date(Date.now() + 60 * 60 * 1000);
const defDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
input.min = toLocalDatetimeInputValue(minDate);
input.value = toLocalDatetimeInputValue(defDate);
picker.style.display = 'flex';
document.getElementById('btnDrawOk').style.display = 'none';
document.getElementById('btnSpeedConfirm').style.display = '';
}
if (dto.card === 'GAME_CARD') {
const btn = document.getElementById('btnDrawOk');
btn.textContent = '▶ Spiel starten';
btn.onclick = function() {
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
};
}
}, 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();
}
function toLocalDatetimeInputValue(date) {
const pad = n => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
async function confirmSpeedCard() {
if (!_pendingSpeedMode) return;
const input = document.getElementById('drawSpeedUntilInput');
if (!input.value) { alert('Bitte wähle einen Zeitpunkt.'); return; }
const until = new Date(input.value);
if (until <= new Date()) { alert('Der Zeitpunkt muss in der Zukunft liegen.'); return; }
const isoUntil = `${input.value}:00`; // datetime-local hat kein Sekunden-Teil
const res = await fetch('/keyholder/cardlock/' + lockId + '/speed/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: _pendingSpeedMode, until: isoUntil })
});
if (!res.ok) { alert('Fehler beim Aktivieren des Speed-Effekts.'); return; }
closeDrawModal();
}
let speedPanelTick = null;
function renderSpeedPanel(lock) {
if (speedPanelTick) { clearInterval(speedPanelTick); speedPanelTick = null; }
const panel = document.getElementById('speedPanel');
const now = new Date();
const slowmoUntil = lock.slowmoUntil ? new Date(lock.slowmoUntil) : null;
const speedupUntil = lock.speedupUntil ? new Date(lock.speedupUntil) : null;
const active = (slowmoUntil && slowmoUntil > now) ? { mode: 'slowmo', until: slowmoUntil }
: (speedupUntil && speedupUntil > now) ? { mode: 'speedup', until: speedupUntil }
: null;
if (!active) { panel.style.display = 'none'; return; }
panel.style.display = 'flex';
document.getElementById('speedPanelTitle').textContent =
active.mode === 'slowmo' ? '🐢 Slow Motion aktiv' : '⚡ Speed Up aktiv';
function tickSpeed() {
const diff = active.until - Date.now();
if (diff <= 0) {
panel.style.display = 'none';
clearInterval(speedPanelTick); speedPanelTick = null;
return;
}
document.getElementById('speedPanelInfo').textContent =
(active.mode === 'slowmo' ? 'Aktionen dauern 4× so lange noch ' : 'Aktionen dauern 4× so kurz noch ') + fmtCountdown(diff);
}
tickSpeed();
speedPanelTick = setInterval(tickSpeed, 1000);
}
// ── 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>