Files
2026-05-05 12:39:20 +02:00

1331 lines
48 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ProofShot — Verification Report</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
background: #0d1117;
color: #c9d1d9;
min-height: 100vh;
}
.header {
padding: 24px 32px;
border-bottom: 1px solid #21262d;
background: #161b22;
}
.header h1 {
font-size: 20px;
font-weight: 600;
color: #f0f6fc;
margin-bottom: 8px;
}
.header .description {
font-size: 14px;
color: #8b949e;
margin-bottom: 6px;
}
.header .description.clamped .description-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.header .description .show-more {
background: none;
border: none;
color: #58a6ff;
font-size: 12px;
cursor: pointer;
padding: 0;
margin-top: 4px;
display: block;
}
.header .description .show-more:hover {
text-decoration: underline;
}
.header .meta {
font-size: 12px;
color: #484f58;
}
/* Overlay toggle controls */
.overlay-toggle {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
position: relative;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
font-size: 12px;
}
.overlay-toggle .tooltip {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 6px;
background: #1c2128;
color: #c9d1d9;
font-size: 11px;
padding: 6px 10px;
border-radius: 6px;
white-space: nowrap;
pointer-events: none;
border: 1px solid #30363d;
z-index: 10;
}
.overlay-toggle:hover .tooltip {
display: block;
}
.overlay-toggle input[type="checkbox"] {
display: none;
}
.toggle-track {
position: relative;
width: 34px;
height: 18px;
background: #30363d;
border-radius: 9px;
transition: background 0.2s;
flex-shrink: 0;
}
.toggle-track::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
background: #8b949e;
border-radius: 50%;
transition: transform 0.2s, background 0.2s;
}
.overlay-toggle input:checked + .toggle-track {
background: #1f6feb;
}
.overlay-toggle input:checked + .toggle-track::after {
transform: translateX(16px);
background: #fff;
}
.error-badges {
display: flex;
gap: 12px;
margin-top: 10px;
}
.error-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
font-family: inherit;
line-height: inherit;
}
.error-badge:hover {
opacity: 0.85;
transform: translateY(-1px);
}
.error-badge.clean {
background: rgba(63, 185, 80, 0.12);
color: #3fb950;
border: 1px solid rgba(63, 185, 80, 0.25);
}
.error-badge.has-errors {
background: rgba(248, 81, 73, 0.12);
color: #f85149;
border: 1px solid rgba(248, 81, 73, 0.25);
}
.error-badge .badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.error-badge.clean .badge-dot {
background: #3fb950;
}
.error-badge.has-errors .badge-dot {
background: #f85149;
}
.viewer {
display: flex;
height: calc(100vh - 180px);
min-height: 400px;
}
.video-panel {
flex: 0 0 62%;
padding: 16px;
display: flex;
align-items: flex-start;
justify-content: center;
background: #0d1117;
overflow: hidden;
}
.video-wrapper {
width: 100%;
display: flex;
flex-direction: column;
}
.video-container {
position: relative;
width: 100%;
}
.video-container video {
width: 100%;
border-radius: 8px 8px 0 0;
background: #000;
display: block;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
border-radius: 8px 8px 0 0;
}
/* Scrub bar */
.scrub-bar {
position: relative;
width: 100%;
padding: 8px 0 6px;
background: #161b22;
border-radius: 0 0 8px 8px;
border-top: 1px solid #21262d;
}
.scrub-track {
position: relative;
height: 6px;
background: #21262d;
border-radius: 3px;
margin: 0 16px;
cursor: pointer;
}
.scrub-track:hover {
height: 8px;
margin-top: -1px;
}
.scrub-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: #58a6ff;
border-radius: 3px;
pointer-events: none;
transition: width 0.1s linear;
}
.scrub-playhead {
position: absolute;
top: 50%;
width: 14px;
height: 14px;
background: #f0f6fc;
border: 2px solid #58a6ff;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 3;
box-shadow: 0 0 4px rgba(0,0,0,0.4);
transition: left 0.1s linear;
}
.scrub-marker {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.scrub-marker-icon {
font-size: 14px;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
background: #21262d;
border: 1.5px solid #30363d;
border-radius: 50%;
transition: all 0.15s;
}
.scrub-marker:hover .scrub-marker-icon,
.scrub-marker.active .scrub-marker-icon {
background: #1f2a37;
border-color: #58a6ff;
transform: scale(1.25);
}
.scrub-tooltip {
display: none;
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 8px;
padding: 6px 10px;
background: #1c2128;
border: 1px solid #30363d;
border-radius: 6px;
font-size: 12px;
color: #c9d1d9;
white-space: nowrap;
pointer-events: none;
z-index: 20;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.scrub-tooltip .tooltip-icon {
margin-right: 4px;
}
.scrub-tooltip .tooltip-time {
color: #58a6ff;
margin-left: 6px;
font-variant-numeric: tabular-nums;
}
.no-video {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 300px;
border: 1px dashed #30363d;
border-radius: 8px;
color: #484f58;
font-size: 15px;
}
.no-video-hint {
font-size: 12px;
margin-top: 8px;
color: #30363d;
}
.timeline-panel {
flex: 0 0 38%;
border-left: 1px solid #21262d;
overflow-y: auto;
background: #161b22;
}
/* Tab bar */
.panel-tabs {
display: flex;
align-items: center;
padding: 0 12px;
border-bottom: 1px solid #21262d;
position: sticky;
top: 0;
background: #161b22;
z-index: 10;
gap: 0;
}
.panel-tab {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: #8b949e;
font-size: 13px;
font-weight: 600;
padding: 10px 16px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.5px;
transition: color 0.15s, border-color 0.15s;
white-space: nowrap;
font-family: inherit;
}
.panel-tab:hover { color: #c9d1d9; }
.panel-tab.active { color: #f0f6fc; border-bottom-color: #58a6ff; }
.panel-tab-actions {
margin-left: auto;
display: flex;
align-items: center;
}
/* Log tab content */
.log-tab-content {
display: flex;
flex-direction: column;
height: 100%;
}
.log-tab-status {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-bottom: 1px solid #21262d;
font-size: 12px;
}
.log-pre {
margin: 0;
padding: 12px 16px;
background: #0d1117;
font-family: 'SF Mono', SFMono-Regular, 'Consolas', 'Liberation Mono', Menlo, monospace;
font-size: 12px;
line-height: 1.6;
color: #c9d1d9;
overflow-x: auto;
white-space: pre;
flex: 1;
overflow-y: auto;
}
.log-pre::-webkit-scrollbar { width: 6px; height: 6px; }
.log-pre::-webkit-scrollbar-track { background: transparent; }
.log-pre::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
.log-pre::-webkit-scrollbar-thumb:hover { background: #484f58; }
.log-line { display: block; }
.log-line[data-time] { cursor: pointer; transition: background 0.15s; padding: 0 4px; margin: 0 -4px; border-radius: 2px; }
.log-line[data-time]:hover { background: rgba(88, 166, 255, 0.08); }
.log-line.active { background: #1f2a37; border-left: 3px solid #58a6ff; padding-left: 1px; }
.log-line.active .log-time { color: #58a6ff; }
.log-time {
display: inline-block;
min-width: 36px;
padding-right: 8px;
text-align: right;
color: #484f58;
user-select: none;
font-variant-numeric: tabular-nums;
font-size: 11px;
}
.log-ln {
display: inline-block;
min-width: 40px;
padding-right: 12px;
text-align: right;
color: #484f58;
user-select: none;
font-variant-numeric: tabular-nums;
}
.log-line-error { background: rgba(248, 81, 73, 0.1); color: #f85149; }
.log-line-error .log-ln { color: rgba(248, 81, 73, 0.5); }
.log-line-error .log-time { color: rgba(248, 81, 73, 0.5); }
.log-empty {
padding: 32px 16px;
text-align: center;
color: #484f58;
font-size: 13px;
font-style: italic;
}
.log-truncated {
padding: 8px 16px;
font-size: 11px;
color: #484f58;
font-style: italic;
border-top: 1px solid #21262d;
background: #161b22;
}
.step {
display: flex;
align-items: center;
padding: 10px 20px;
cursor: pointer;
border-bottom: 1px solid #21262d;
transition: background 0.15s;
gap: 10px;
}
.step:hover {
background: #1c2128;
}
.step.active {
background: #1f2a37;
border-left: 3px solid #58a6ff;
padding-left: 17px;
}
.step-number {
font-size: 11px;
color: #484f58;
min-width: 20px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.icon {
font-size: 16px;
min-width: 24px;
text-align: center;
}
.step-content {
flex: 1;
min-width: 0;
}
.action {
font-size: 13px;
font-family: 'SF Mono', SFMono-Regular, 'Consolas', 'Liberation Mono', Menlo, monospace;
color: #c9d1d9;
word-break: break-all;
}
.step.active .action {
color: #f0f6fc;
}
.time {
font-size: 12px;
color: #484f58;
font-variant-numeric: tabular-nums;
min-width: 36px;
text-align: right;
}
.step.active .time {
color: #58a6ff;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #484f58;
font-size: 14px;
}
/* Overlay animations */
.ripple {
position: absolute;
border-radius: 50%;
pointer-events: none;
transform: translate(-50%, -50%);
animation: ripple-expand 600ms ease-out forwards;
}
@keyframes ripple-expand {
0% { width: 12px; height: 12px; opacity: 0.7; }
100% { width: 60px; height: 60px; opacity: 0; }
}
.ripple-click { background: rgba(56, 132, 255, 0.5); }
.ripple-fill { background: rgba(255, 152, 56, 0.5); }
.scroll-indicator {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
opacity: 0.6;
pointer-events: none;
animation: fade-out 800ms ease-out forwards;
}
@keyframes fade-out {
0% { opacity: 0.6; }
100% { opacity: 0; }
}
.toast {
position: absolute;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 10px 20px;
border-radius: 10px;
font-size: 15px;
font-weight: 500;
pointer-events: none;
animation: toast-in 200ms ease-out;
white-space: nowrap;
letter-spacing: 0.2px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
@keyframes toast-in {
0% { opacity: 0; transform: translateX(-50%) translateY(8px); }
100% { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Scrollbar styling */
.timeline-panel::-webkit-scrollbar { width: 6px; }
.timeline-panel::-webkit-scrollbar-track { background: transparent; }
.timeline-panel::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
.timeline-panel::-webkit-scrollbar-thumb:hover { background: #484f58; }
@media (max-width: 768px) {
.viewer {
flex-direction: column;
height: auto;
}
.video-panel, .timeline-panel {
flex: none;
width: 100%;
}
.timeline-panel {
border-left: none;
border-top: 1px solid #21262d;
max-height: 50vh;
}
.error-badges {
flex-wrap: wrap;
}
.log-pre {
max-height: 50vh;
}
}
</style>
</head>
<body>
<div class="header">
<h1><svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px;vertical-align:middle;margin-right:8px"><path d="M8,24 L8,12 C8,8 12,8 12,8 L24,8" fill="none" stroke="#6366F1" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path d="M40,8 L52,8 C56,8 56,12 56,12 L56,24" fill="none" stroke="#6366F1" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8,40 L8,52 C8,56 12,56 12,56 L24,56" fill="none" stroke="#6366F1" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path d="M40,56 L52,56 C56,56 56,52 56,52 L56,40" fill="none" stroke="#6366F1" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20,34 L28,42 L44,22" fill="none" stroke="#22D3EE" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></svg>ProofShot Verification</h1>
<p class="description" id="description"><span class="description-text">Verify B: CSS strikethrough for struck-through rule, no fake input for illegible</span><button class="show-more" id="showMoreBtn" style="display:none" onclick="toggleDescription()">Show more</button></p>
<p class="meta">2026-04-25 11:04:14 &middot; 88s</p>
<div class="error-badges">
<button class="error-badge clean" onclick="switchTab('console')"><span class="badge-dot"></span>Console: clean</button>
<button class="error-badge clean" onclick="switchTab('server')"><span class="badge-dot"></span>Server: clean</button>
</div>
</div>
<div class="viewer">
<div class="video-panel">
<div class="video-wrapper">
<div class="video-container">
<video src="./session.webm" controls></video>
<div class="video-overlay"></div>
</div>
<div class="scrub-bar">
<div class="scrub-track" id="scrubTrack">
<div class="scrub-progress" id="scrubProgress"></div>
<div class="scrub-playhead" id="scrubPlayhead"></div>
<div class="scrub-marker" data-index="0" data-time="2.8" style="left:3.1818181818181817%"><span class="scrub-marker-icon">🧭</span></div>
<div class="scrub-marker" data-index="1" data-time="3.3" style="left:3.75%"><span class="scrub-marker-icon">📷</span></div>
<div class="scrub-marker" data-index="2" data-time="6.5" style="left:7.386363636363637%"><span class="scrub-marker-icon">👁</span></div>
<div class="scrub-marker" data-index="3" data-time="10.7" style="left:12.159090909090908%"><span class="scrub-marker-icon"></span></div>
<div class="scrub-marker" data-index="4" data-time="11.5" style="left:13.068181818181818%"><span class="scrub-marker-icon"></span></div>
<div class="scrub-marker" data-index="5" data-time="12.7" style="left:14.431818181818182%"><span class="scrub-marker-icon">🖱</span></div>
<div class="scrub-marker" data-index="6" data-time="14.9" style="left:16.93181818181818%"><span class="scrub-marker-icon">🧭</span></div>
<div class="scrub-marker" data-index="7" data-time="15.9" style="left:18.06818181818182%"><span class="scrub-marker-icon">📷</span></div>
<div class="scrub-marker" data-index="8" data-time="27.8" style="left:31.590909090909093%"><span class="scrub-marker-icon">👁</span></div>
<div class="scrub-marker" data-index="9" data-time="38.6" style="left:43.86363636363637%"><span class="scrub-marker-icon">🖱</span></div>
<div class="scrub-marker" data-index="10" data-time="39" style="left:44.31818181818182%"><span class="scrub-marker-icon">📷</span></div>
<div class="scrub-marker" data-index="11" data-time="62.2" style="left:70.68181818181819%"><span class="scrub-marker-icon">🧭</span></div>
<div class="scrub-marker" data-index="12" data-time="63.7" style="left:72.38636363636364%"><span class="scrub-marker-icon">📷</span></div>
<div class="scrub-marker" data-index="13" data-time="77.7" style="left:88.29545454545455%"><span class="scrub-marker-icon"></span></div>
<div class="scrub-marker" data-index="14" data-time="80.7" style="left:91.70454545454547%"><span class="scrub-marker-icon"></span></div>
<div class="scrub-marker" data-index="15" data-time="81" style="left:92.04545454545455%"><span class="scrub-marker-icon">📷</span></div>
</div>
<div class="scrub-tooltip" id="scrubTooltip"></div>
</div>
</div>
</div>
<div class="timeline-panel">
<div class="panel-tabs">
<button class="panel-tab active" data-tab="timeline" onclick="switchTab('timeline')">Timeline &middot; 16</button>
<button class="panel-tab" data-tab="console" onclick="switchTab('console')">Console &middot; 13</button>
<button class="panel-tab" data-tab="server" onclick="switchTab('server')">Server</button>
<div class="panel-tab-actions" id="tabActionsTimeline">
<label class="overlay-toggle"><input type="checkbox" id="toggle-overlays" checked><span class="toggle-track"></span> Overlays<span class="tooltip">Show ripple animations and action labels on the video as each step plays.</span></label>
</div>
</div>
<div id="tabTimeline">
<div class="step" data-time="2.8" data-index="0" onclick="seekTo(2.8)">
<span class="step-number">1</span>
<span class="icon">🧭</span>
<div class="step-content">
<span class="action">open http://localhost:5173/hilfe/transkription</span>
</div>
<span class="time">0:02</span>
</div>
<div class="step" data-time="3.3" data-index="1" onclick="seekTo(3.3)">
<span class="step-number">2</span>
<span class="icon">📷</span>
<div class="step-content">
<span class="action">screenshot step-rules.png</span>
</div>
<span class="time">0:03</span>
</div>
<div class="step" data-time="6.5" data-index="2" onclick="seekTo(6.5)">
<span class="step-number">3</span>
<span class="icon">👁</span>
<div class="step-content">
<span class="action">snapshot -i</span>
</div>
<span class="time">0:06</span>
</div>
<div class="step" data-time="10.7" data-index="3" onclick="seekTo(10.7)">
<span class="step-number">4</span>
<span class="icon"></span>
<div class="step-content">
<span class="action">fill @e6 admin@familyarchive.local</span>
</div>
<span class="time">0:10</span>
</div>
<div class="step" data-time="11.5" data-index="4" onclick="seekTo(11.5)">
<span class="step-number">5</span>
<span class="icon"></span>
<div class="step-content">
<span class="action">fill @e7 admin123</span>
</div>
<span class="time">0:11</span>
</div>
<div class="step" data-time="12.7" data-index="5" onclick="seekTo(12.7)">
<span class="step-number">6</span>
<span class="icon">🖱</span>
<div class="step-content">
<span class="action">click @e8</span>
</div>
<span class="time">0:12</span>
</div>
<div class="step" data-time="14.9" data-index="6" onclick="seekTo(14.9)">
<span class="step-number">7</span>
<span class="icon">🧭</span>
<div class="step-content">
<span class="action">open http://localhost:5173/hilfe/transkription</span>
</div>
<span class="time">0:14</span>
</div>
<div class="step" data-time="15.9" data-index="7" onclick="seekTo(15.9)">
<span class="step-number">8</span>
<span class="icon">📷</span>
<div class="step-content">
<span class="action">screenshot step-rules.png</span>
</div>
<span class="time">0:15</span>
</div>
<div class="step" data-time="27.8" data-index="8" onclick="seekTo(27.8)">
<span class="step-number">9</span>
<span class="icon">👁</span>
<div class="step-content">
<span class="action">snapshot</span>
</div>
<span class="time">0:27</span>
</div>
<div class="step" data-time="38.6" data-index="9" onclick="seekTo(38.6)">
<span class="step-number">10</span>
<span class="icon">🖱</span>
<div class="step-content">
<span class="action">click @e17</span>
</div>
<span class="time">0:38</span>
</div>
<div class="step" data-time="39" data-index="10" onclick="seekTo(39)">
<span class="step-number">11</span>
<span class="icon">📷</span>
<div class="step-content">
<span class="action">screenshot step-strikethrough.png</span>
</div>
<span class="time">0:39</span>
</div>
<div class="step" data-time="62.2" data-index="11" onclick="seekTo(62.2)">
<span class="step-number">12</span>
<span class="icon">🧭</span>
<div class="step-content">
<span class="action">open http://localhost:5173/hilfe/transkription</span>
</div>
<span class="time">1:02</span>
</div>
<div class="step" data-time="63.7" data-index="12" onclick="seekTo(63.7)">
<span class="step-number">13</span>
<span class="icon">📷</span>
<div class="step-content">
<span class="action">screenshot --full-page step-full.png</span>
</div>
<span class="time">1:03</span>
</div>
<div class="step" data-time="77.7" data-index="13" onclick="seekTo(77.7)">
<span class="step-number">14</span>
<span class="icon"></span>
<div class="step-content">
<span class="action">scroll 0 500</span>
</div>
<span class="time">1:17</span>
</div>
<div class="step" data-time="80.7" data-index="14" onclick="seekTo(80.7)">
<span class="step-number">15</span>
<span class="icon"></span>
<div class="step-content">
<span class="action">scroll down</span>
</div>
<span class="time">1:20</span>
</div>
<div class="step" data-time="81" data-index="15" onclick="seekTo(81)">
<span class="step-number">16</span>
<span class="icon">📷</span>
<div class="step-content">
<span class="action">screenshot step-scrolled.png</span>
</div>
<span class="time">1:21</span>
</div>
</div>
<div id="tabConsole" style="display:none">
<div class="log-tab-content">
<div class="log-tab-status">
<span class="error-badge clean" style="cursor:default"><span class="badge-dot"></span>Console: clean</span>
</div>
<pre class="log-pre"><span class="log-line" data-time="0" onclick="seekTo(0)"><span class="log-time">0:00</span><span class="log-ln">1</span>[debug] [vite] connecting...</span>
<span class="log-line" data-time="0" onclick="seekTo(0)"><span class="log-time">0:00</span><span class="log-ln">2</span>[debug] [vite] connected.</span>
<span class="log-line" data-time="0" onclick="seekTo(0)"><span class="log-time">0:00</span><span class="log-ln">3</span>[debug] [vite] connecting...</span>
<span class="log-line" data-time="0.1" onclick="seekTo(0.1)"><span class="log-time">0:00</span><span class="log-ln">4</span>[debug] [vite] connected.</span>
<span class="log-line" data-time="3.1" onclick="seekTo(3.1)"><span class="log-time">0:03</span><span class="log-ln">5</span>[debug] [vite] connecting...</span>
<span class="log-line" data-time="3.1" onclick="seekTo(3.1)"><span class="log-time">0:03</span><span class="log-ln">6</span>[debug] [vite] connected.</span>
<span class="log-line" data-time="15" onclick="seekTo(15)"><span class="log-time">0:15</span><span class="log-ln">7</span>[debug] [vite] connecting...</span>
<span class="log-line" data-time="15" onclick="seekTo(15)"><span class="log-time">0:15</span><span class="log-ln">8</span>[debug] [vite] connected.</span>
<span class="log-line" data-time="15.6" onclick="seekTo(15.6)"><span class="log-time">0:15</span><span class="log-ln">9</span>[error] Failed to fetch unread count TypeError: Failed to fetch
at window.fetch (http://localhost:5173/node_modules/@sveltejs/kit/src/runtime/client/fetcher.js?v=9aff597a:66:10)
at fetchUnreadCount (http://localhost:5173/src/lib/stores/notifications.svelte.ts?t=1777114433090:31:46)
at Object.init (http://localhost:5173/src/lib/stores/notifications.svelte.ts?t=1777114433090:74:2)
at http://localhost:5173/src/lib/components/NotificationBell.svelte?t=1777114433090:71:10
at untrack (http://localhost:5173/node_modules/.vite/deps/chunk-W6TT6Y22.js?v=9aff597a:4131:12)
at $effect (http://localhost:5173/node_modules/.vite/deps/chunk-XZDNTRGL.js?v=9aff597a:4165:23)
at update_reaction (http://localhost:5173/node_modules/.vite/deps/chunk-W6TT6Y22.js?v=9aff597a:3831:18)
at update_effect (http://localhost:5173/node_modules/.vite/deps/chunk-W6TT6Y22.js?v=9aff597a:3970:21)
at flush_queued_effects (http://localhost:5173/node_modules/.vite/deps/chunk-W6TT6Y22.js?v=9aff597a:2955:7)
at #process (http://localhost:5173/node_modules/.vite/deps/chunk-W6TT6Y22.js?v=9aff597a:2613:7)</span>
<span class="log-line" data-time="15.8" onclick="seekTo(15.8)"><span class="log-time">0:15</span><span class="log-ln">10</span>[debug] [vite] connecting...</span>
<span class="log-line" data-time="15.8" onclick="seekTo(15.8)"><span class="log-time">0:15</span><span class="log-ln">11</span>[debug] [vite] connected.</span>
<span class="log-line" data-time="62.6" onclick="seekTo(62.6)"><span class="log-time">1:02</span><span class="log-ln">12</span>[debug] [vite] connecting...</span>
<span class="log-line" data-time="62.6" onclick="seekTo(62.6)"><span class="log-time">1:02</span><span class="log-ln">13</span>[debug] [vite] connected.</span></pre>
</div>
</div>
<div id="tabServer" style="display:none">
<div class="log-tab-content">
<div class="log-tab-status">
<span class="error-badge clean" style="cursor:default"><span class="badge-dot"></span>Server: clean</span>
</div>
<p class="log-empty">No server log captured</p>
</div>
</div>
</div>
</div>
<script>
// --- Description expand/collapse ---
function initDescription() {
const desc = document.getElementById('description');
const btn = document.getElementById('showMoreBtn');
if (!desc || !btn) return;
const textEl = desc.querySelector('.description-text');
// Clamp initially, then check if text overflows
desc.classList.add('clamped');
requestAnimationFrame(() => {
if (textEl.scrollHeight > textEl.clientHeight + 1) {
btn.style.display = 'block';
}
});
}
function toggleDescription() {
const desc = document.getElementById('description');
const btn = document.getElementById('showMoreBtn');
if (!desc || !btn) return;
const isClamped = desc.classList.contains('clamped');
desc.classList.toggle('clamped');
btn.textContent = isClamped ? 'Show less' : 'Show more';
}
initDescription();
// --- Tab switching ---
let activeTab = 'timeline';
function switchTab(tab) {
if (tab === activeTab) return;
activeTab = tab;
document.querySelectorAll('.panel-tab').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
document.getElementById('tabTimeline').style.display = tab === 'timeline' ? '' : 'none';
document.getElementById('tabConsole').style.display = tab === 'console' ? '' : 'none';
document.getElementById('tabServer').style.display = tab === 'server' ? '' : 'none';
var actions = document.getElementById('tabActionsTimeline');
if (actions) actions.style.display = tab === 'timeline' ? '' : 'none';
}
const video = document.querySelector('video');
const steps = document.querySelectorAll('.step');
const timelinePanel = document.querySelector('.timeline-panel');
const overlay = document.querySelector('.video-overlay');
const entries = [{"action":"open http://localhost:5173/hilfe/transkription","relativeTimeSec":2.8,"timestamp":"2026-04-25T11:02:46.720Z"},{"action":"screenshot step-rules.png","relativeTimeSec":3.3,"timestamp":"2026-04-25T11:02:47.146Z"},{"action":"snapshot -i","relativeTimeSec":6.5,"timestamp":"2026-04-25T11:02:50.423Z"},{"action":"fill @e6 admin@familyarchive.local","relativeTimeSec":10.7,"timestamp":"2026-04-25T11:02:54.622Z","element":{"label":"E-Mail-Adresse","bbox":{"x":481,"y":314,"width":318,"height":42},"viewport":{"width":1280,"height":720}}},{"action":"fill @e7 admin123","relativeTimeSec":11.5,"timestamp":"2026-04-25T11:02:55.421Z","element":{"label":"Passwort","bbox":{"x":481,"y":398,"width":318,"height":42},"viewport":{"width":1280,"height":720}}},{"action":"click @e8","relativeTimeSec":12.7,"timestamp":"2026-04-25T11:02:56.543Z"},{"action":"open http://localhost:5173/hilfe/transkription","relativeTimeSec":14.9,"timestamp":"2026-04-25T11:02:58.745Z"},{"action":"screenshot step-rules.png","relativeTimeSec":15.9,"timestamp":"2026-04-25T11:02:59.804Z"},{"action":"snapshot","relativeTimeSec":27.8,"timestamp":"2026-04-25T11:03:11.712Z"},{"action":"click @e17","relativeTimeSec":38.6,"timestamp":"2026-04-25T11:03:22.492Z","element":{"label":"Durchgestrichene Wörter","bbox":{"x":366,"y":711.5,"width":174,"height":24},"viewport":{"width":1280,"height":720}}},{"action":"screenshot step-strikethrough.png","relativeTimeSec":39,"timestamp":"2026-04-25T11:03:22.830Z"},{"action":"open http://localhost:5173/hilfe/transkription","relativeTimeSec":62.2,"timestamp":"2026-04-25T11:03:46.063Z"},{"action":"screenshot --full-page step-full.png","relativeTimeSec":63.7,"timestamp":"2026-04-25T11:03:47.577Z"},{"action":"scroll 0 500","relativeTimeSec":77.7,"timestamp":"2026-04-25T11:04:01.584Z"},{"action":"scroll down","relativeTimeSec":80.7,"timestamp":"2026-04-25T11:04:04.579Z"},{"action":"screenshot step-scrolled.png","relativeTimeSec":81,"timestamp":"2026-04-25T11:04:04.856Z"}];
let duration = 88;
const markers = [{"time":2.8,"icon":"🧭","action":"open http://localhost:5173/hilfe/transkription","index":0},{"time":3.3,"icon":"📷","action":"screenshot step-rules.png","index":1},{"time":6.5,"icon":"👁","action":"snapshot -i","index":2},{"time":10.7,"icon":"⌨","action":"fill @e6 admin@familyarchive.local","index":3},{"time":11.5,"icon":"⌨","action":"fill @e7 admin123","index":4},{"time":12.7,"icon":"🖱","action":"click @e8","index":5},{"time":14.9,"icon":"🧭","action":"open http://localhost:5173/hilfe/transkription","index":6},{"time":15.9,"icon":"📷","action":"screenshot step-rules.png","index":7},{"time":27.8,"icon":"👁","action":"snapshot","index":8},{"time":38.6,"icon":"🖱","action":"click @e17","index":9},{"time":39,"icon":"📷","action":"screenshot step-strikethrough.png","index":10},{"time":62.2,"icon":"🧭","action":"open http://localhost:5173/hilfe/transkription","index":11},{"time":63.7,"icon":"📷","action":"screenshot --full-page step-full.png","index":12},{"time":77.7,"icon":"↕","action":"scroll 0 500","index":13},{"time":80.7,"icon":"↕","action":"scroll down","index":14},{"time":81,"icon":"📷","action":"screenshot step-scrolled.png","index":15}];
// Scrub bar elements
const scrubTrack = document.getElementById('scrubTrack');
const scrubProgress = document.getElementById('scrubProgress');
const scrubPlayhead = document.getElementById('scrubPlayhead');
const scrubTooltip = document.getElementById('scrubTooltip');
const scrubMarkers = document.querySelectorAll('.scrub-marker');
// --- Toggle state ---
const toggleOverlays = document.getElementById('toggle-overlays');
function loadToggleState() {
try {
const saved = JSON.parse(localStorage.getItem('proofshot-overlays') || '{}');
if (saved.overlays === false) toggleOverlays.checked = false;
} catch {}
}
function saveToggleState() {
try {
localStorage.setItem('proofshot-overlays', JSON.stringify({
overlays: toggleOverlays.checked,
}));
} catch {}
}
loadToggleState();
toggleOverlays.addEventListener('change', () => {
if (!toggleOverlays.checked) clearOverlays();
saveToggleState();
});
function clearOverlays() {
if (!overlay) return;
overlay.querySelectorAll('.ripple, .scroll-indicator, .toast').forEach(el => el.remove());
}
// --- Action icon (mirrors server-side getActionIcon) ---
function getActionIconJS(action) {
const cmd = action.split(' ')[0].toLowerCase();
switch (cmd) {
case 'open': case 'navigate': return '\u{1F9ED}';
case 'click': return '\u{1F5B1}';
case 'fill': case 'type': return '\u2328';
case 'screenshot': return '\u{1F4F7}';
case 'snapshot': return '\u{1F441}';
case 'scroll': return '\u2195';
case 'press': case 'keyboard': return '\u2318';
default: return '\u25B6';
}
}
// --- Toast text generation ---
function getToastText(entry) {
const action = entry.action;
const parts = action.split(' ');
const cmd = parts[0].toLowerCase();
const label = entry.element ? entry.element.label : '';
const icon = getActionIconJS(action);
switch (cmd) {
case 'click':
return icon + ' Click' + (label ? ': ' + label : '');
case 'fill': {
const valMatch = action.match(/"([^"]*)"/);
const val = valMatch ? valMatch[1] : '';
const target = label || '';
return icon + ' Type: ' + val + (target ? ' into ' + target : '');
}
case 'type': {
const valMatch2 = action.match(/"([^"]*)"/);
const val2 = valMatch2 ? valMatch2[1] : '';
const target2 = label || '';
return icon + ' Type: ' + val2 + (target2 ? ' into ' + target2 : '');
}
case 'scroll': {
const dir = parts[1] || '';
return icon + ' Scroll ' + dir;
}
case 'open': {
const url = parts.slice(1).join(' ');
try {
return icon + ' Navigate: ' + new URL(url).pathname;
} catch {
return icon + ' Navigate: ' + url;
}
}
case 'press':
return icon + ' Press: ' + parts.slice(1).join(' ');
case 'screenshot':
return icon + ' Screenshot';
default:
return icon + ' ' + action;
}
}
// --- Scroll direction arrows ---
function getScrollArrow(action) {
const parts = action.split(' ');
const dir = (parts[1] || '').toLowerCase();
switch (dir) {
case 'up': return '\u2191';
case 'down': return '\u2193';
case 'left': return '\u2190';
case 'right': return '\u2192';
default: return '\u2195';
}
}
// --- Overlay scheduling ---
const overlayWindows = entries.map((entry, i) => {
const cmd = entry.action.split(' ')[0].toLowerCase();
const nextTime = i + 1 < entries.length ? entries[i + 1].relativeTimeSec : entry.relativeTimeSec + 3;
const rippleEnd = entry.relativeTimeSec + 0.6;
const toastEnd = Math.min(nextTime, entry.relativeTimeSec + 3);
const scrollEnd = entry.relativeTimeSec + 0.8;
return {
entry,
cmd,
rippleStart: entry.relativeTimeSec,
rippleEnd: cmd === 'scroll' ? scrollEnd : rippleEnd,
toastStart: entry.relativeTimeSec,
toastEnd,
};
});
const activeRipples = new Map();
const activeToasts = new Map();
let rafId = null;
function renderOverlays() {
if (!video || !overlay) return;
const t = video.currentTime;
const videoEl = video;
overlayWindows.forEach((win, idx) => {
const enabled = toggleOverlays.checked;
// --- Ripple / scroll indicator ---
if (enabled) {
if (t >= win.rippleStart && t < win.rippleEnd && !activeRipples.has(idx)) {
const el = document.createElement('div');
if (win.cmd === 'scroll') {
el.className = 'scroll-indicator';
el.textContent = getScrollArrow(win.entry.action);
overlay.appendChild(el);
activeRipples.set(idx, el);
} else if ((win.cmd === 'click' || win.cmd === 'fill' || win.cmd === 'type') && win.entry.element) {
const elem = win.entry.element;
const scaleX = videoEl.clientWidth / elem.viewport.width;
const scaleY = videoEl.clientHeight / elem.viewport.height;
const cx = (elem.bbox.x + elem.bbox.width / 2) * scaleX;
const cy = (elem.bbox.y + elem.bbox.height / 2) * scaleY;
el.className = 'ripple ' + (win.cmd === 'click' ? 'ripple-click' : 'ripple-fill');
el.style.left = cx + 'px';
el.style.top = cy + 'px';
overlay.appendChild(el);
activeRipples.set(idx, el);
}
}
if (t >= win.rippleEnd && activeRipples.has(idx)) {
activeRipples.get(idx).remove();
activeRipples.delete(idx);
}
} else if (activeRipples.has(idx)) {
activeRipples.get(idx).remove();
activeRipples.delete(idx);
}
// --- Toast ---
if (enabled) {
if (t >= win.toastStart && t < win.toastEnd && !activeToasts.has(idx)) {
activeToasts.forEach((el) => el.remove());
activeToasts.clear();
const el = document.createElement('div');
el.className = 'toast';
el.textContent = getToastText(win.entry);
overlay.appendChild(el);
activeToasts.set(idx, el);
}
if (t >= win.toastEnd && activeToasts.has(idx)) {
activeToasts.get(idx).remove();
activeToasts.delete(idx);
}
} else if (activeToasts.has(idx)) {
activeToasts.get(idx).remove();
activeToasts.delete(idx);
}
});
rafId = requestAnimationFrame(renderOverlays);
}
function startOverlayLoop() {
if (rafId !== null) return;
rafId = requestAnimationFrame(renderOverlays);
}
function stopOverlayLoop() {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
}
// --- Seek handler: clear overlays on seek so they re-trigger correctly ---
function onSeeked() {
activeRipples.forEach(el => el.remove());
activeRipples.clear();
activeToasts.forEach(el => el.remove());
activeToasts.clear();
}
function seekTo(time) {
if (video) {
video.currentTime = time;
video.play();
}
}
function formatTimeFn(sec) {
const m = Math.floor(sec / 60);
const s = Math.floor(sec % 60);
return m + ':' + String(s).padStart(2, '0');
}
// Update scrub bar position
function updateScrubBar(t) {
if (!scrubTrack || duration <= 0) return;
const pct = Math.min((t / duration) * 100, 100);
if (scrubProgress) scrubProgress.style.width = pct + '%';
if (scrubPlayhead) scrubPlayhead.style.left = pct + '%';
}
// Highlight active marker on scrub bar
function updateActiveMarker(t) {
scrubMarkers.forEach(m => {
const mTime = parseFloat(m.dataset.time);
const idx = parseInt(m.dataset.index);
const nextMarker = markers[idx + 1];
const nextTime = nextMarker ? nextMarker.time : Infinity;
m.classList.toggle('active', t >= mTime && t < nextTime);
});
}
// Scrub bar: click track to seek
if (scrubTrack && video) {
let isDragging = false;
function getTimeFromEvent(e) {
const rect = scrubTrack.getBoundingClientRect();
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
return pct * duration;
}
scrubTrack.addEventListener('mousedown', (e) => {
if (e.target.closest('.scrub-marker')) return;
isDragging = true;
const t = getTimeFromEvent(e);
video.currentTime = t;
updateScrubBar(t);
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const t = getTimeFromEvent(e);
video.currentTime = t;
updateScrubBar(t);
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
video.play();
}
});
}
// Scrub bar: marker hover tooltips
scrubMarkers.forEach(marker => {
marker.addEventListener('mouseenter', (e) => {
const idx = parseInt(marker.dataset.index);
const m = markers[idx];
if (!m || !scrubTooltip) return;
const action = m.action.length > 40 ? m.action.slice(0, 40) + '\u2026' : m.action;
scrubTooltip.innerHTML = '<span class="tooltip-icon">' + m.icon + '</span>' + action + '<span class="tooltip-time">' + formatTimeFn(m.time) + '</span>';
scrubTooltip.style.display = 'block';
const trackRect = scrubTrack.getBoundingClientRect();
const markerRect = marker.getBoundingClientRect();
const tooltipLeft = markerRect.left - trackRect.left + markerRect.width / 2;
scrubTooltip.style.left = tooltipLeft + 'px';
scrubTooltip.style.transform = 'translateX(-50%)';
});
marker.addEventListener('mouseleave', () => {
if (scrubTooltip) scrubTooltip.style.display = 'none';
});
marker.addEventListener('click', (e) => {
e.stopPropagation();
const t = parseFloat(marker.dataset.time);
seekTo(t);
});
});
// Log lines with timestamps for video sync
const logLines = document.querySelectorAll('.log-line[data-time]');
// Highlight active log line for a given video time
function updateActiveLogLine(t) {
logLines.forEach(line => {
const lt = parseFloat(line.dataset.time);
const nextLine = line.nextElementSibling;
const hasNext = nextLine && nextLine.dataset && nextLine.dataset.time !== undefined;
const nextTime = hasNext ? parseFloat(nextLine.dataset.time) : Infinity;
line.classList.toggle('active', t >= lt && t < nextTime);
});
// Auto-scroll the active log line in the currently visible tab
if (activeTab === 'console' || activeTab === 'server') {
var tabId = activeTab === 'console' ? 'tabConsole' : 'tabServer';
var tabEl = document.getElementById(tabId);
if (tabEl) {
var activeLine = tabEl.querySelector('.log-line.active');
if (activeLine && timelinePanel) {
var panelRect = timelinePanel.getBoundingClientRect();
var lineRect = activeLine.getBoundingClientRect();
if (lineRect.top < panelRect.top || lineRect.bottom > panelRect.bottom) {
activeLine.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}
}
}
}
// Highlight active step as video plays (only if video exists)
if (video) {
video.addEventListener('timeupdate', () => {
const t = video.currentTime;
let activeStep = null;
steps.forEach(step => {
const stepTime = parseFloat(step.dataset.time);
const nextStep = step.nextElementSibling;
const isLastStep = !nextStep || !nextStep.classList.contains('step');
const nextTime = isLastStep ? Infinity : parseFloat(nextStep.dataset.time);
const isActive = t >= stepTime && t < nextTime;
step.classList.toggle('active', isActive);
if (isActive) activeStep = step;
});
// Auto-scroll the active step into view (only when timeline tab is active)
if (activeStep && activeTab === 'timeline') {
const panelRect = timelinePanel.getBoundingClientRect();
const stepRect = activeStep.getBoundingClientRect();
if (stepRect.top < panelRect.top || stepRect.bottom > panelRect.bottom) {
activeStep.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}
// Sync log lines with video
updateActiveLogLine(t);
// Update scrub bar + markers
updateScrubBar(t);
updateActiveMarker(t);
});
// Sync scrub bar duration with actual video duration
video.addEventListener('loadedmetadata', () => {
if (video.duration && isFinite(video.duration)) {
duration = video.duration;
// Reposition markers to match actual video duration
scrubMarkers.forEach(m => {
const mTime = parseFloat(m.dataset.time);
m.style.left = (duration > 0 ? (mTime / duration) * 100 : 0) + '%';
});
}
});
// Start/stop rAF overlay loop with video play state
video.addEventListener('play', startOverlayLoop);
video.addEventListener('pause', stopOverlayLoop);
video.addEventListener('ended', stopOverlayLoop);
video.addEventListener('seeked', onSeeked);
}
// Keyboard navigation: left/right arrows jump between steps
document.addEventListener('keydown', (e) => {
if (activeTab !== 'timeline') return;
if (!video || !markers.length) return;
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault();
const t = video.currentTime;
let targetIdx = -1;
if (e.key === 'ArrowRight') {
// Find next marker after current time
for (let i = 0; i < markers.length; i++) {
if (markers[i].time > t + 0.5) { targetIdx = i; break; }
}
if (targetIdx === -1) targetIdx = markers.length - 1;
} else {
// Find previous marker before current time
for (let i = markers.length - 1; i >= 0; i--) {
if (markers[i].time < t - 0.5) { targetIdx = i; break; }
}
if (targetIdx === -1) targetIdx = 0;
}
seekTo(markers[targetIdx].time);
}
});
</script>
</body>
</html>