Files
xxx-sphere-web/bin/main/static/games/chastity/activetimelock.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

1383 lines
65 KiB
HTML
Raw Permalink 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>TimeLock 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; }
/* ── Zeit-Panel ── */
.time-panel {
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 10px; padding: 1.25rem 1.25rem 1rem;
margin-bottom: 1.25rem; text-align: center; position: relative; overflow: hidden;
}
.time-panel.frozen {
border-color: rgba(100,180,255,0.45);
background:
radial-gradient(ellipse at 20% 20%, rgba(255,255,255,0.18) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(255,255,255,0.12) 0%, transparent 45%),
rgba(30,80,140,0.35);
}
.time-panel.time-up {
border-color: rgba(46,204,113,0.5);
background: rgba(46,204,113,0.05);
}
.time-panel-label {
font-size: 0.72rem; font-weight: 700; color: var(--color-muted);
text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.5rem;
}
.time-countdown {
font-size: 3rem; font-weight: 700; font-family: monospace;
color: var(--color-text); letter-spacing: 0.05em; line-height: 1.1;
}
.time-countdown.time-up-display {
font-size: 1.8rem; color: #2ecc71;
}
.frozen-badge {
display: inline-flex; align-items: center; gap: 0.4rem;
background: rgba(100,180,255,0.15); border: 1px solid rgba(100,180,255,0.35);
border-radius: 20px; padding: 0.3rem 0.9rem; font-size: 0.88rem;
color: #7ec8ff; margin-top: 0.6rem;
}
.frozen-until-text {
font-size: 0.8rem; color: var(--color-muted); margin-top: 0.35rem;
font-family: monospace;
}
/* ── Spin-Panel ── */
.spin-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;
}
.spin-panel-title {
font-size: 0.75rem; font-weight: 700; color: var(--color-muted);
text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.25rem;
}
.spin-countdown {
font-size: 1.4rem; font-weight: 700; font-family: monospace;
color: var(--color-text);
}
.btn-spin {
white-space: nowrap; width: auto; padding: 0.5rem 1.1rem; font-size: 0.9rem;
transition: opacity 0.2s, background 0.2s;
}
.btn-spin:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
/* ── Aufgaben-Panel ── */
.task-panel {
background: var(--color-card);
border: 1px solid rgba(100,180,255,0.4);
background-image:
radial-gradient(ellipse at 18% 28%, rgba(255,255,255,0.22) 0%, transparent 48%),
radial-gradient(ellipse at 82% 72%, rgba(255,255,255,0.16) 0%, transparent 42%),
linear-gradient(rgba(55,140,210,0.12), rgba(55,140,210,0.12));
border-radius: 10px; padding: 1rem 1.1rem; margin-bottom: 1.25rem;
}
.task-panel-title {
font-size: 0.75rem; font-weight: 700; color: rgba(100,180,255,0.9);
text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.5rem;
}
.task-text {
font-size: 1rem; color: var(--color-text); line-height: 1.5; margin-bottom: 0.5rem;
}
.task-description {
font-size: 0.88rem; color: var(--color-muted); line-height: 1.4; margin-bottom: 0.6rem;
}
.task-until {
font-size: 1rem; font-weight: 700; font-family: monospace;
color: var(--color-primary); margin-bottom: 0.6rem;
}
.btn-task-done {
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-done:disabled {
background: rgba(255,255,255,0.15); 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; }
/* ── Glücksrad-Animation ── */
.wheel-anim-backdrop {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.82); z-index: 600;
align-items: center; justify-content: center;
}
.wheel-anim-backdrop.open { display: flex; }
.wheel-canvas-wrap {
position: relative; display: flex; flex-direction: column; align-items: center;
}
.wheel-pointer-top {
font-size: 2.2rem; line-height: 1; margin-bottom: -12px; z-index: 1;
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.8));
color: #e74c3c;
}
#wheelCanvas { border-radius: 50%; display: block; }
/* ── Glücksrad-Ergebnis (unterhalb des Rads) ── */
.wheel-result {
margin-top: 1.25rem; width: 290px;
display: flex; flex-direction: column; align-items: center;
gap: 0.6rem; text-align: center;
}
.spin-result-icon { font-size: 3rem; }
.spin-result-title { font-size: 1.1rem; font-weight: 700; margin: 0; color: var(--color-text); }
.spin-result-desc { font-size: 0.9rem; color: var(--color-muted); line-height: 1.5; margin: 0; }
/* ── Hygiene-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;
}
/* ── 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; }
.btn-lock-unlock {
background: rgba(46,204,113,0.15); border: 1px solid rgba(46,204,113,0.45);
color: #2ecc71; font-size: 0.95rem; font-weight: 700; padding: 0.65rem 1.5rem;
border-radius: 8px; cursor: pointer; width: auto; transition: background 0.15s;
}
.btn-lock-unlock:hover { background: rgba(46,204,113,0.28); }
.btn-confirm-unlock {
background: rgba(52,152,219,0.15); border: 1px solid rgba(52,152,219,0.45);
color: #3498db; font-size: 0.88rem; font-weight: 600; padding: 0.55rem 1.25rem;
border-radius: 8px; cursor: pointer; width: auto; transition: background 0.15s;
}
.btn-confirm-unlock:hover { background: rgba(52,152,219,0.28); }
.unlock-confirmed-badge {
font-size: 0.82rem; color: #2ecc71;
border: 1px solid rgba(46,204,113,0.35); border-radius: 7px;
padding: 0.35rem 0.75rem; display: inline-block;
}
.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;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h2 style="margin-bottom:1.25rem;">⏱ TimeLock Session</h2>
<!-- Keyholder-Bar -->
<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>
<!-- Keyholder-Bestätigung ausstehend -->
<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>
<!-- Zeit-Panel -->
<div class="time-panel" id="timePanel">
<div class="time-panel-label" id="timePanelLabel">Verbleibende Zeit</div>
<div class="time-countdown" id="timeCountdown"></div>
<div class="frozen-badge" id="frozenBadge" style="display:none;">❄️ Eingefroren</div>
<div class="frozen-until-text" id="frozenUntilText" style="display:none;"></div>
</div>
<!-- Spin-Panel -->
<div class="spin-panel" id="spinPanel" style="display:none;">
<div>
<div class="spin-panel-title">Glücksrad</div>
<div class="spin-countdown" id="spinCountdown"></div>
</div>
<button class="btn-spin" id="spinBtn" onclick="doSpin()">🎡 Drehen</button>
</div>
<!-- Aufgaben-Panel -->
<div class="task-panel" id="taskPanel" style="display:none;">
<div class="task-panel-title">🎯 Aktuelle Aufgabe</div>
<div class="task-text" id="taskText"></div>
<div class="task-description" id="taskDescription" style="display:none;"></div>
<div class="task-until" id="taskUntilDisplay" style="display:none;"></div>
<button class="btn-task-done" id="btnTaskDone" disabled onclick="taskErledigt()">✓ Erledigt</button>
</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>
<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>
<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>
<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;flex-direction:column;align-items:flex-end;gap:0.5rem;margin-top:2rem;"></div>
</div>
</div>
<!-- Glücksrad-Animation-Modal -->
<div class="wheel-anim-backdrop" id="wheelAnimModal">
<div class="wheel-canvas-wrap">
<div class="wheel-pointer-top"></div>
<canvas id="wheelCanvas" width="290" height="290"></canvas>
<div class="wheel-result" id="wheelResult" style="display:none;">
<div class="spin-result-icon" id="spinResultIcon"></div>
<h3 class="spin-result-title" id="spinResultTitle"></h3>
<p class="spin-result-desc" id="spinResultDesc"></p>
<button onclick="closeSpinModal()" style="width:100%;margin-top:0.25rem;">OK</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 id="hygieneModalTitle">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;background:var(--color-primary);color:#fff;border:none;padding:0.55rem 1.5rem;border-radius:8px;cursor:pointer;font-weight:600;" onclick="endHygieneOpening()">✓ Beenden</button>
</div>
<!-- Phase 2: Neuer Code -->
<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 id="hygienePhase2Btn" onclick="startHygieneScramble()" style="background:var(--color-primary);color:#fff;border:none;padding:0.55rem 1.5rem;border-radius:8px;cursor:pointer;font-weight:600;">OK</button>
</div>
<!-- Phase 3: TTLock-Kommunikation -->
<div id="hygienePhase3" style="display:none;width:100%;flex-direction:column;align-items:center;gap:0.5rem;padding:0.5rem 0;">
<div style="font-size:1.5rem;">🔗</div>
<div style="font-size:0.9rem;color:var(--color-muted);">Kommuniziere mit TTLock-Server…</div>
<div style="font-size:0.8rem;color:var(--color-muted);font-family:monospace;">Bitte warten</div>
</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 style="font-size:1rem;font-weight:700;">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 style="background:none;border:1px solid var(--color-secondary);color:var(--color-muted);padding:0.5rem 1rem;border-radius:7px;cursor:pointer;width:auto;" onclick="closeVerificationModal()">Abbrechen</button>
<button style="background:var(--color-primary);color:#fff;border:none;padding:0.5rem 1rem;border-radius:7px;cursor:pointer;width:auto;" id="verificationDoneBtn" disabled onclick="completeVerification()">✓ Fertig</button>
</div>
</div>
</div>
<!-- Unlock-Result-Modal -->
<div class="warn-modal-backdrop" id="unlockResultModal">
<div class="warn-modal-box" style="align-items:center;text-align:center;">
<div style="font-size:2.5rem;line-height:1;">🔓</div>
<h3 style="margin:0;">Lock entsperrt</h3>
<div id="unlockResultBody" style="width:100%;"></div>
<div class="warn-modal-actions" style="justify-content:center;flex-wrap:wrap;margin-top:0.5rem;">
<button class="btn-cancel" onclick="document.getElementById('unlockResultModal').classList.remove('open')">Schließen</button>
<button style="background:#27ae60;border:none;color:#fff;padding:0.5rem 1.1rem;border-radius:7px;cursor:pointer;font-size:0.88rem;font-weight:600;width:auto;" onclick="lockLoeschen()">Lock beenden</button>
</div>
</div>
</div>
<!-- Warn-Modal (Lock 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>
<script src="/js/shared.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');
let _currentLock = null;
let tickInterval = null;
let frozenTickInterval = null;
let spinTickInterval = null;
let hygienePanelTick = null;
let taskTickInterval = null;
let hygieneTickInterval = null;
let hygieneScrambleTimer = null;
let hygieneScrambleCd = null;
let currentVerificationId = null;
let _verificationBlob = null;
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/timelock/' + lockId);
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);
renderTimePanel(lock);
renderSpinPanel(lock);
renderTaskPanel(lock);
renderHygienePanel(lock);
renderVerificationPanel(lock);
renderLockActionArea(lock);
}
// ── Format-Helpers ──────────────────────────────────────────────────────────
function fmtCountdown(diffMs) {
const totalSec = Math.max(0, Math.floor(diffMs / 1000));
const d = Math.floor(totalSec / 86400);
const h = Math.floor((totalSec % 86400) / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
if (d > 0)
return `${d}d ${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
return h > 0
? `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`
: `${m}:${String(s).padStart(2,'0')}`;
}
function fmtDateTime(iso) {
if (!iso) return '';
const dt = new Date(iso);
return dt.toLocaleDateString('de-DE') + ', ' + dt.toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' }) + ' Uhr';
}
// ── 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 = '👤';
}
}
// ── Zeit-Panel ─────────────────────────────────────────────────────────────
function renderTimePanel(lock) {
if (tickInterval) { clearInterval(tickInterval); tickInterval = null; }
if (frozenTickInterval) { clearInterval(frozenTickInterval); frozenTickInterval = null; }
const panel = document.getElementById('timePanel');
const label = document.getElementById('timePanelLabel');
const display = document.getElementById('timeCountdown');
const badge = document.getElementById('frozenBadge');
const untilTxt = document.getElementById('frozenUntilText');
panel.classList.remove('frozen', 'time-up');
badge.style.display = 'none';
untilTxt.style.display = 'none';
display.classList.remove('time-up-display');
if (lock.timeUp) {
panel.classList.add('time-up');
label.textContent = 'Entsperrzeit erreicht';
display.classList.add('time-up-display');
display.textContent = '✓ Bereit';
return;
}
if (lock.isFrozen) {
panel.classList.add('frozen');
badge.style.display = '';
if (lock.frozenUntil) {
const fu = new Date(lock.frozenUntil);
untilTxt.textContent = 'bis ' + fmtDateTime(lock.frozenUntil);
untilTxt.style.display = '';
}
}
if (!lock.unlockTime) {
label.textContent = 'Restzeit';
display.textContent = '⏱ nicht sichtbar';
return;
}
label.textContent = 'Verbleibende Zeit';
const target = new Date(lock.unlockTime);
function tick() {
const diff = target - Date.now();
if (diff <= 0) {
display.classList.add('time-up-display');
display.textContent = '✓ Bereit';
panel.classList.remove('frozen');
panel.classList.add('time-up');
label.textContent = 'Entsperrzeit erreicht';
clearInterval(tickInterval); tickInterval = null;
loadLock();
return;
}
display.textContent = fmtCountdown(diff);
}
tick();
tickInterval = setInterval(tick, 1000);
}
// ── Glücksrad-Animation ─────────────────────────────────────────────────────
let wheelEntries = [];
let wheelResult = null;
let wheelAnimFrame = null;
let wheelLastTime = null;
let wheelAngle = 0;
let wheelAnimState = 'idle';
let wheelSpinStartTime = 0;
let wheelDecelStartTime = 0;
let wheelDecelStartAngle = 0;
let wheelTargetAngle = 0;
const WHEEL_MAX_SPEED = 0.014; // rad/ms
const WHEEL_ACCEL_MS = 500;
const WHEEL_DECEL_MS = 2500;
const WHEEL_MIN_SPIN_MS = 1000;
const WHEEL_COLORS = [
'#c0392b','#2980b9','#27ae60','#d35400',
'#8e44ad','#16a085','#e67e22','#c0392b',
'#1565c0','#2e7d32'
];
const WHEEL_LABELS = {
ADD_TIME: '+ Zeit',
REMOVE_TIME: ' Zeit',
FREEZE_TIME: '❄ Einfrieren',
FREEZE: '🧊 Freeze',
UNFREEZE: '🌊 Auftauen',
TASK: '🎯 Aufgabe',
TEXT: '💬 Text',
};
function startWheelSpin() {
document.getElementById('wheelResult').style.display = 'none';
document.getElementById('wheelAnimModal').classList.add('open');
wheelAngle = Math.random() * 2 * Math.PI;
wheelResult = null;
wheelAnimState = 'accelerating';
wheelLastTime = null;
wheelSpinStartTime = performance.now();
if (wheelAnimFrame) cancelAnimationFrame(wheelAnimFrame);
wheelAnimFrame = requestAnimationFrame(wheelTick);
}
function wheelTick(now) {
if (!wheelLastTime) wheelLastTime = now;
const dt = Math.min(now - wheelLastTime, 50);
wheelLastTime = now;
const canvas = document.getElementById('wheelCanvas');
if (!canvas || !wheelEntries.length) return;
if (wheelAnimState === 'accelerating') {
const p = Math.min((now - wheelSpinStartTime) / WHEEL_ACCEL_MS, 1);
wheelAngle += WHEEL_MAX_SPEED * p * dt;
if (p >= 1) { wheelAnimState = 'spinning'; wheelSpinStartTime = now; }
} else if (wheelAnimState === 'spinning') {
wheelAngle += WHEEL_MAX_SPEED * dt;
if (wheelResult && (now - wheelSpinStartTime) >= WHEEL_MIN_SPIN_MS) {
initWheelDecel();
}
} else if (wheelAnimState === 'decelerating') {
const p = Math.min((now - wheelDecelStartTime) / WHEEL_DECEL_MS, 1);
const eased = 1 - Math.pow(1 - p, 4);
wheelAngle = wheelDecelStartAngle + eased * (wheelTargetAngle - wheelDecelStartAngle);
if (p >= 1) {
wheelAngle = wheelTargetAngle;
wheelAnimState = 'done';
drawWheelFrame(canvas, wheelAngle);
setTimeout(() => {
showSpinResult(wheelResult);
}, 750);
return;
}
}
drawWheelFrame(canvas, wheelAngle);
wheelAnimFrame = requestAnimationFrame(wheelTick);
}
function initWheelDecel() {
const n = wheelEntries.length;
let idx = wheelEntries.findIndex(e =>
e.type === wheelResult.type &&
e.intVal === wheelResult.intVal &&
e.stringVal === wheelResult.stringVal
);
if (idx < 0) idx = wheelEntries.findIndex(e => e.type === wheelResult.type);
if (idx < 0) idx = 0;
const segCenter = ((idx + 0.5) / n) * 2 * Math.PI;
const rawTarget = -Math.PI / 2 - segCenter;
const targetMod = ((rawTarget % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI);
const currMod = ((wheelAngle % (2*Math.PI)) + 2*Math.PI) % (2*Math.PI);
let extra = targetMod - currMod;
if (extra < 0) extra += 2 * Math.PI;
wheelDecelStartAngle = wheelAngle;
wheelTargetAngle = wheelAngle + extra + 2 * 2 * Math.PI;
wheelDecelStartTime = performance.now();
wheelAnimState = 'decelerating';
}
function drawWheelFrame(canvas, angle) {
const ctx = canvas.getContext('2d');
const w = canvas.width, h = canvas.height;
const cx = w/2, cy = h/2;
const r = Math.min(w, h)/2 - 6;
const n = wheelEntries.length;
if (!n) return;
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < n; i++) {
const a0 = angle + (i / n) * 2 * Math.PI;
const a1 = angle + ((i+1) / n) * 2 * Math.PI;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, a0, a1);
ctx.closePath();
ctx.fillStyle = WHEEL_COLORS[i % WHEEL_COLORS.length];
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 2;
ctx.stroke();
const aMid = angle + ((i+0.5) / n) * 2 * Math.PI;
const fontSize = Math.max(9, Math.min(13, Math.floor(170 / n)));
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(aMid);
ctx.textAlign = 'right';
ctx.fillStyle = '#fff';
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 3;
ctx.fillText(WHEEL_LABELS[wheelEntries[i].type] || wheelEntries[i].type, r - 10, 4);
ctx.restore();
}
// Outer ring
ctx.beginPath();
ctx.arc(cx, cy, r, 0, 2*Math.PI);
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
ctx.lineWidth = 5;
ctx.stroke();
// Center circle
ctx.beginPath();
ctx.arc(cx, cy, 17, 0, 2*Math.PI);
ctx.fillStyle = '#111122';
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.lineWidth = 2;
ctx.stroke();
}
// ── Spin-Panel ─────────────────────────────────────────────────────────────
function renderSpinPanel(lock) {
wheelEntries = lock.spinningWheelEntries || [];
if (spinTickInterval) { clearInterval(spinTickInterval); spinTickInterval = null; }
const panel = document.getElementById('spinPanel');
const cdEl = document.getElementById('spinCountdown');
const btn = document.getElementById('spinBtn');
if (!lock.spinEnabled) { panel.style.display = 'none'; return; }
panel.style.display = '';
if (lock.spinDue) {
cdEl.textContent = 'Bereit';
cdEl.style.color = '#2ecc71';
btn.disabled = lock.isFrozen || lock.hygieneOpeningActive || false;
return;
}
btn.disabled = true;
cdEl.style.color = '';
if (!lock.nextSpinIn) { cdEl.textContent = ''; return; }
const target = new Date(lock.nextSpinIn);
function tick() {
const diff = target - Date.now();
if (diff <= 0) {
cdEl.textContent = 'Bereit';
cdEl.style.color = '#2ecc71';
btn.disabled = lock.isFrozen || false;
clearInterval(spinTickInterval); spinTickInterval = null;
return;
}
cdEl.textContent = fmtCountdown(diff);
}
tick();
spinTickInterval = setInterval(tick, 1000);
}
async function doSpin() {
const btn = document.getElementById('spinBtn');
btn.disabled = true;
startWheelSpin();
try {
const res = await fetch('/keyholder/timelock/' + lockId + '/spin', { method: 'POST' });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
document.getElementById('wheelAnimModal').classList.remove('open');
if (wheelAnimFrame) { cancelAnimationFrame(wheelAnimFrame); wheelAnimFrame = null; }
wheelAnimState = 'idle';
alert(data.error || 'Spin nicht möglich.');
btn.disabled = false;
return;
}
wheelResult = await res.json();
// Animation läuft weiter Ergebnis wird nach Abschluss angezeigt
} catch(e) {
document.getElementById('wheelAnimModal').classList.remove('open');
if (wheelAnimFrame) { cancelAnimationFrame(wheelAnimFrame); wheelAnimFrame = null; }
wheelAnimState = 'idle';
alert('Fehler beim Drehen.');
btn.disabled = false;
}
}
const SPIN_DISPLAY = {
ADD_TIME: { icon: '⏰', title: 'Zeit hinzugefügt', descFn: r => `+${fmtMinutes(r.intVal)} wurden zum Lock hinzugefügt.` },
REMOVE_TIME: { icon: '⚡', title: 'Zeit entfernt', descFn: r => `${fmtMinutes(r.intVal)} wurden vom Lock abgezogen.` },
FREEZE_TIME: { icon: '❄️', title: 'Eingefroren!', descFn: r => `Das Lock ist für ${fmtMinutes(r.intVal)} eingefroren.` },
FREEZE: { icon: '🧊', title: 'Eingefroren!', descFn: () => 'Das Lock ist eingefroren, bis es manuell entsperrt wird.' },
UNFREEZE: { icon: '🌊', title: 'Entsperrt', descFn: () => 'Das Lock wurde aufgetaut.' },
TASK: { icon: '🎯', title: 'Aufgabe!', descFn: r => r.stringVal || 'Neue Aufgabe erhalten.' },
TEXT: { icon: '💬', title: 'Nachricht', descFn: r => r.stringVal || '' },
};
function fmtMinutes(m) {
if (!m) return '0 Min.';
const d = Math.floor(m / 1440);
const h = Math.floor((m % 1440) / 60);
const min = m % 60;
const parts = [];
if (d) parts.push(d + ' Tag' + (d !== 1 ? 'e' : ''));
if (h) parts.push(h + ' Std');
if (min) parts.push(min + ' Min.');
return parts.join(' ') || '0 Min.';
}
function showSpinResult(result) {
const info = SPIN_DISPLAY[result.type] || { icon: '🎡', title: result.type, descFn: r => JSON.stringify(r) };
document.getElementById('spinResultIcon').textContent = info.icon;
document.getElementById('spinResultTitle').textContent = info.title;
document.getElementById('spinResultDesc').textContent = info.descFn(result);
document.getElementById('wheelResult').style.display = '';
}
function closeSpinModal() {
document.getElementById('wheelAnimModal').classList.remove('open');
document.getElementById('wheelResult').style.display = 'none';
loadLock();
}
// ── Aufgaben-Panel ─────────────────────────────────────────────────────────
function renderTaskPanel(lock) {
if (taskTickInterval) { clearInterval(taskTickInterval); taskTickInterval = null; }
const panel = document.getElementById('taskPanel');
const textEl = document.getElementById('taskText');
const descEl = document.getElementById('taskDescription');
const untilEl = document.getElementById('taskUntilDisplay');
const btn = document.getElementById('btnTaskDone');
if (!lock.currentTask) { panel.style.display = 'none'; return; }
panel.style.display = '';
textEl.textContent = lock.currentTask;
if (lock.currentTaskDescription) {
descEl.textContent = lock.currentTaskDescription;
descEl.style.display = '';
} else {
descEl.style.display = 'none';
}
if (lock.taskUntil) {
const deadline = new Date(lock.taskUntil);
if (deadline > new Date()) {
btn.disabled = true;
untilEl.style.display = '';
function tickTask() {
const diff = deadline - Date.now();
if (diff <= 0) {
btn.disabled = false;
untilEl.style.display = 'none';
clearInterval(taskTickInterval); taskTickInterval = null;
return;
}
untilEl.textContent = '⏱ noch ' + fmtCountdown(diff);
}
tickTask();
taskTickInterval = setInterval(tickTask, 1000);
return;
}
}
btn.disabled = false;
untilEl.style.display = 'none';
}
async function taskErledigt() {
const btn = document.getElementById('btnTaskDone');
btn.disabled = true;
await fetch('/keyholder/timelock/' + lockId + '/task/done', { method: 'POST' });
loadLock();
}
// ── Hygiene-Panel ──────────────────────────────────────────────────────────
function renderHygienePanel(lock) {
if (hygienePanelTick) { clearInterval(hygienePanelTick); hygienePanelTick = null; }
if (!lock.hygieneEnabled) { document.getElementById('hygienePanel').style.display = 'none'; return; }
const panel = document.getElementById('hygienePanel');
const countdown = document.getElementById('hygieneCountdown');
const btn = document.getElementById('hygieneBtn');
panel.style.display = '';
btn.style.display = '';
if (lock.hygieneOpeningActive) {
countdown.textContent = 'Aktiv';
btn.disabled = false;
return;
}
if (lock.hygieneOpeningDue) {
countdown.textContent = 'Verfügbar';
btn.disabled = false;
return;
}
btn.disabled = true;
let totalSeconds = lock.hygieneSecondsRemaining || 0;
function tick() {
if (totalSeconds <= 0) {
countdown.textContent = 'Verfügbar';
btn.disabled = false;
clearInterval(hygienePanelTick); hygienePanelTick = null;
return;
}
countdown.textContent = fmtCountdown(totalSeconds * 1000);
totalSeconds--;
}
tick();
hygienePanelTick = setInterval(tick, 1000);
}
async function openHygieneModal() {
const res = await fetch('/keyholder/timelock/' + 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('hygieneModalTitle').textContent = 'Hygiene-Öffnung';
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; }
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
document.getElementById('hygienePhase1').style.display = 'none';
document.getElementById('hygienePhase3').style.display = 'flex';
}
const res = await fetch('/keyholder/timelock/' + lockId + '/hygiene/end', { method: 'POST' });
if (!res.ok) {
document.getElementById('hygienePhase3').style.display = 'none';
document.getElementById('hygienePhase1').style.display = 'flex';
alert('Fehler beim Beenden der Hygiene-Öffnung.');
return;
}
const data = await res.json();
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
closeHygieneModal();
return;
}
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();
}
function startHygieneScramble() {
const codeEl = document.getElementById('hygieneNewCode');
const hintEl = document.getElementById('hygienePhase2Hint');
const cdEl = document.getElementById('hygieneScrambleCountdown');
const btnEl = document.getElementById('hygienePhase2Btn');
const len = codeEl.textContent.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
function finish() {
stopped = true;
clearInterval(hygieneScrambleTimer); hygieneScrambleTimer = null;
clearInterval(hygieneScrambleCd); hygieneScrambleCd = null;
closeHygieneModal();
}
hintEl.style.display = 'none';
cdEl.style.display = '';
document.getElementById('hygieneModalTitle').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);
}
// ── Verifikation ──────────────────────────────────────────────────────────
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) {
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) {
dueBody.style.display = 'none';
pendingBody.style.display = '';
doneBody.style.display = 'none';
} else {
dueBody.style.display = '';
pendingBody.style.display = 'none';
doneBody.style.display = 'none';
}
}
async function startVerification() {
let verificationId, code;
if (window._pendingVerificationId) {
verificationId = window._pendingVerificationId;
code = window._pendingVerificationCode;
} else {
const res = await fetch('/keyholder/timelock/' + 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';
}
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 sc = MAX / Math.max(w, h); w = Math.round(w*sc); h = Math.round(h*sc); }
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/timelock/' + 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';
}
async function renewVerification() {
const btn = document.getElementById('btnVerificationRenew');
btn.disabled = true;
const res = await fetch('/keyholder/timelock/' + lockId + '/verification/today', { method: 'DELETE' });
if (res.ok || res.status === 204) loadLock();
else btn.disabled = false;
}
// ── Lock-Aktionen ──────────────────────────────────────────────────────────
function renderLockActionArea(lock) {
const area = document.getElementById('lockActionArea');
if (!area) return;
if (lock.timeUp) {
if (!lock.actualUnlockTime) {
area.innerHTML = `<button class="btn-lock-unlock" onclick="confirmUnlock()">🔓 Entsperren</button>`;
} else {
area.innerHTML = `<button class="btn-lock-unlock" onclick="openUnlockResultModal()">🔓 Lock abschließen</button>`;
}
} else if (lock.keyholderRequestedUnlock) {
const code = lock.unlockCode || '';
const isEmergency = lock.emergencyAutoUnlocked;
const title = isEmergency
? '🆘 Lock automatisch freigegeben'
: '🔓 Dein Keyholder hat das Lock freigegeben!';
const note = isEmergency
? `<div style="font-size:0.8rem;color:var(--color-muted);margin-top:0.4rem;">
Da dein Keyholder nicht innerhalb einer Stunde reagiert hat, wurde das Lock automatisch geöffnet. Es werden keine XP vergeben und kein Eintrag in die Historie gespeichert.
</div>`
: '';
area.innerHTML = `
<div style="background:rgba(46,204,113,0.06);border:1px solid rgba(46,204,113,0.3);border-radius:10px;padding:1rem 1.1rem;">
<div style="font-weight:700;font-size:0.95rem;color:#2ecc71;margin-bottom:0.6rem;">${title}</div>
<div style="font-size:0.72rem;font-weight:700;color:var(--color-muted);text-transform:uppercase;letter-spacing:0.07em;margin-bottom:0.3rem;">Dein Entsperrcode</div>
<div style="font-size:2rem;font-weight:700;font-family:monospace;letter-spacing:0.2em;
background:rgba(233,69,96,0.08);border:1px solid rgba(233,69,96,0.25);
border-radius:8px;padding:0.5rem 1rem;color:var(--color-primary);text-align:center;">${code}</div>
${note}
<button class="btn-lock-beenden" style="margin-top:0.75rem;" onclick="lockLoeschen()">🔓 Lock beenden</button>
</div>`;
} else if (lock.testLock) {
area.innerHTML = `<button class="btn-lock-beenden" onclick="lockBeendenFragen()">🔓 Lock beenden</button>`;
} else if (lock.emergencyUnlockRequested) {
const msg = 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);">${msg}</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>`;
}
}
async function confirmUnlock() {
const btn = document.querySelector('#lockActionArea .btn-lock-unlock');
if (btn) btn.disabled = true;
try {
const res = await fetch('/keyholder/timelock/' + lockId + '/unlock-time', { method: 'PATCH' });
if (res.ok || res.status === 204) {
const r2 = await fetch('/keyholder/timelock/' + lockId);
if (r2.ok) _currentLock = await r2.json();
renderLockActionArea(_currentLock);
openUnlockResultModal();
} else {
if (btn) btn.disabled = false;
}
} catch(_) {
if (btn) btn.disabled = false;
}
}
function openUnlockResultModal() {
const lock = _currentLock;
const body = document.getElementById('unlockResultBody');
if (lock && lock.controllType === 'TRUST') {
body.innerHTML = `<p style="margin:0.5rem 0 0;color:#2ecc71;font-weight:600;font-size:1rem;">✓ Du kannst dich jetzt befreien.</p>`;
} else {
const code = (lock && lock.unlockCode) || '';
body.innerHTML = `
<div style="margin-top:0.75rem;">
<div style="font-size:0.75rem;color:var(--color-muted);text-transform:uppercase;letter-spacing:0.07em;margin-bottom:0.4rem;">Entsperrcode</div>
<div style="font-size:2.2rem;font-weight:700;font-family:monospace;letter-spacing:0.22em;
background:rgba(233,69,96,0.08);border:1px solid rgba(233,69,96,0.25);
border-radius:8px;padding:0.6rem 1.4rem;color:var(--color-primary);">${code}</div>
</div>`;
}
document.getElementById('unlockResultModal').classList.add('open');
}
function lockBeendenFragen() {
document.getElementById('warnModalUnlockCode').textContent = _currentLock ? (_currentLock.unlockCode || '') : '';
document.getElementById('warnModal').classList.add('open');
document.querySelector('#warnModal h3').textContent = '🔓 Lock beenden?';
}
function closeWarnModal() {
document.getElementById('warnModal').classList.remove('open');
}
async function lockLoeschen() {
closeWarnModal();
if (_currentLock && _currentLock.controllType === 'TTLOCK') {
document.getElementById('unlockResultBody').innerHTML = `
<div style="margin-top:0.75rem;display:flex;flex-direction:column;align-items:center;gap:0.5rem;">
<div style="font-size:1.5rem;">🔗</div>
<div style="font-size:0.9rem;color:var(--color-muted);">Kommuniziere mit TTLock-Server…</div>
<div style="font-size:0.8rem;color:var(--color-muted);font-family:monospace;">Bitte warten</div>
</div>`;
// Buttons im Modal ausblenden während kommuniziert wird
document.querySelector('#unlockResultModal .warn-modal-actions').style.display = 'none';
document.getElementById('unlockResultModal').classList.add('open');
}
try {
await fetch('/keyholder/timelock/' + lockId, { method: 'DELETE' });
} catch(_) { /* ignorieren */ }
window.location.href = '/userhome.html';
}
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/timelock/' + lockId + '/emergency-unlock', { method: 'POST' });
if (res.status === 200) {
// Kein Keyholder: Entsperrcode direkt anzeigen, Lock als ungültig markiert
const data = await res.json().catch(() => ({}));
document.getElementById('emergencyModalContent').innerHTML = `
<p style="font-size:0.85rem;color:var(--color-muted);line-height:1.5;margin:0 0 0.75rem;">
⚠️ Das Lock wird als <strong style="color:var(--color-text);">ungültig</strong> gewertet
kein Historieneintrag, keine XP.
</p>
<div style="font-size:2rem;font-weight:700;font-family:monospace;letter-spacing:0.2em;
text-align:center;background:var(--color-secondary);border-radius:8px;
padding:0.75rem 1rem;color:var(--color-primary);margin-bottom:0.5rem;">
${data.unlockCode || ''}
</div>`;
const btn = document.getElementById('btnEmergencyConfirm');
btn.textContent = 'OK Lock beenden';
btn.onclick = async () => { closeEmergencyModal(); await lockLoeschen(); };
document.getElementById('emergencyModalActions').style.display = '';
} else if (res.status === 204) {
// Keyholder vorhanden: Benachrichtigung gesendet
const khName = _currentLock && _currentLock.keyholderName;
document.getElementById('emergencyModalContent').innerHTML =
`<p style="font-size:0.88rem;color:#2ecc71;line-height:1.5;margin:0;">
✅ Notfall-Anfrage gesendet. ${khName} wurde benachrichtigt.
</p>`;
setTimeout(() => { closeEmergencyModal(); loadLock(); }, 2000);
}
} catch(e) {
document.getElementById('emergencyModalContent').innerHTML =
`<p style="color:#e74c3c;">Fehler beim Senden der Anfrage.</p>`;
}
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
closeWarnModal();
closeEmergencyModal();
closeSpinModal();
document.getElementById('unlockResultModal').classList.remove('open');
}
});
// ── Auto-Refresh alle 60 Sekunden ──
setInterval(loadLock, 60_000);
</script>
</body>
</html>