fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,425 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Video - BreakPilot H5P</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
padding: 20px;
min-height: 100vh;
}
.container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #333;
margin-bottom: 12px;
font-size: 28px;
}
.description {
color: #6b7280;
margin-bottom: 24px;
font-size: 16px;
line-height: 1.6;
}
.video-wrapper {
position: relative;
background: #000;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16/9;
margin-bottom: 24px;
}
.video-wrapper iframe,
.video-wrapper video {
width: 100%;
height: 100%;
border: none;
}
.interaction-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: none;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 100;
}
.interaction-overlay.show {
display: flex;
}
.interaction-content {
background: white;
border-radius: 12px;
padding: 32px;
max-width: 600px;
width: 100%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.interaction-icon {
font-size: 48px;
text-align: center;
margin-bottom: 16px;
}
.interaction-title {
font-size: 24px;
font-weight: 700;
color: #1f2937;
margin-bottom: 16px;
text-align: center;
}
.interaction-text {
color: #4b5563;
font-size: 16px;
line-height: 1.6;
margin-bottom: 24px;
}
.interaction-link {
display: block;
padding: 12px 24px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
text-align: center;
font-weight: 600;
margin-bottom: 12px;
}
.interaction-link:hover {
background: #5568d3;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
width: 100%;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.timeline {
background: #f9fafb;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.timeline-title {
font-weight: 700;
color: #374151;
margin-bottom: 12px;
font-size: 14px;
}
.timeline-markers {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.timeline-marker {
background: #fef3c7;
border: 2px solid #f59e0b;
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
color: #92400e;
cursor: pointer;
transition: all 0.2s;
}
.timeline-marker:hover {
background: #fde68a;
transform: translateY(-2px);
}
.timeline-marker.completed {
background: #d1fae5;
border-color: #10b981;
color: #065f46;
}
.controls {
text-align: center;
color: #6b7280;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1 id="title"></h1>
<p class="description" id="description"></p>
<div class="video-wrapper">
<div id="videoContainer"></div>
<div class="interaction-overlay" id="interactionOverlay">
<div class="interaction-content">
<div class="interaction-icon" id="interactionIcon"></div>
<div class="interaction-title" id="interactionTitle"></div>
<div class="interaction-text" id="interactionText"></div>
<div id="interactionExtra"></div>
<button class="btn btn-primary" onclick="closeInteraction()">Weiter</button>
</div>
</div>
</div>
<div class="timeline">
<div class="timeline-title">📌 Interaktive Elemente im Video:</div>
<div class="timeline-markers" id="timelineMarkers"></div>
</div>
<div class="controls">
Das Video pausiert automatisch bei interaktiven Elementen
</div>
</div>
<script src="https://www.youtube.com/iframe_api"></script>
<script src="https://player.vimeo.com/api/player.js"></script>
<script>
let data = null;
let player = null;
let playerType = null;
let currentTime = 0;
let completedInteractions = new Set();
let checkInterval = null;
function loadContent() {
const urlParams = new URLSearchParams(window.location.search);
const dataParam = urlParams.get('data');
if (dataParam) {
try {
data = JSON.parse(decodeURIComponent(dataParam));
} catch (e) {
alert('Fehler beim Laden!');
return;
}
}
if (!data) {
alert('Keine Daten gefunden!');
return;
}
document.getElementById('title').textContent = data.title;
document.getElementById('description').textContent = data.description || '';
renderTimeline();
embedVideo();
}
function embedVideo() {
const container = document.getElementById('videoContainer');
const url = data.videoUrl;
// YouTube
if (url.includes('youtube.com') || url.includes('youtu.be')) {
const videoId = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)?.[1];
if (videoId) {
playerType = 'youtube';
container.innerHTML = `<div id="ytPlayer"></div>`;
// YouTube API will be loaded via script tag
window.onYouTubeIframeAPIReady = function() {
player = new YT.Player('ytPlayer', {
videoId: videoId,
width: '100%',
height: '100%',
playerVars: {
autoplay: 0,
modestbranding: 1,
rel: 0
},
events: {
onReady: function() {
startTimeTracking();
},
onStateChange: function(event) {
if (event.data === YT.PlayerState.PLAYING) {
startTimeTracking();
}
}
}
});
};
}
}
// Vimeo
else if (url.includes('vimeo.com')) {
const videoId = url.match(/vimeo\.com\/(\d+)/)?.[1];
if (videoId) {
playerType = 'vimeo';
container.innerHTML = `<iframe id="vimeoPlayer" src="https://player.vimeo.com/video/${videoId}" allowfullscreen></iframe>`;
setTimeout(() => {
player = new Vimeo.Player('vimeoPlayer');
player.on('timeupdate', function(data) {
currentTime = data.seconds;
checkInteractions();
});
}, 500);
}
}
// Direct MP4
else if (url.endsWith('.mp4')) {
playerType = 'html5';
container.innerHTML = `<video id="html5Player" controls style="width: 100%; height: 100%;"><source src="${url}" type="video/mp4"></video>`;
const video = document.getElementById('html5Player');
video.addEventListener('timeupdate', function() {
currentTime = video.currentTime;
checkInteractions();
});
player = video;
}
}
function startTimeTracking() {
if (checkInterval) clearInterval(checkInterval);
checkInterval = setInterval(() => {
if (playerType === 'youtube' && player && player.getCurrentTime) {
currentTime = player.getCurrentTime();
checkInteractions();
}
}, 500);
}
function checkInteractions() {
data.interactions.forEach((interaction, index) => {
const interactionTime = timeToSeconds(interaction.time);
// Check if we're at this interaction time (within 1 second window)
if (Math.abs(currentTime - interactionTime) < 0.5 && !completedInteractions.has(index)) {
showInteraction(interaction, index);
}
});
}
function showInteraction(interaction, index) {
// Pause video
if (playerType === 'youtube' && player && player.pauseVideo) {
player.pauseVideo();
} else if (playerType === 'vimeo' && player && player.pause) {
player.pause();
} else if (playerType === 'html5' && player && player.pause) {
player.pause();
}
// Show overlay
const overlay = document.getElementById('interactionOverlay');
const icon = document.getElementById('interactionIcon');
const title = document.getElementById('interactionTitle');
const text = document.getElementById('interactionText');
const extra = document.getElementById('interactionExtra');
// Set icon based on type
switch (interaction.type) {
case 'question':
icon.textContent = '❓';
break;
case 'info':
icon.textContent = '';
break;
case 'link':
icon.textContent = '🔗';
break;
}
title.textContent = interaction.title;
text.textContent = interaction.content;
// Add link if type is link
if (interaction.type === 'link') {
extra.innerHTML = `<a href="${interaction.content}" target="_blank" class="interaction-link">🔗 Link öffnen</a>`;
} else {
extra.innerHTML = '';
}
overlay.classList.add('show');
completedInteractions.add(index);
// Update timeline
renderTimeline();
}
function closeInteraction() {
const overlay = document.getElementById('interactionOverlay');
overlay.classList.remove('show');
// Resume video
if (playerType === 'youtube' && player && player.playVideo) {
player.playVideo();
} else if (playerType === 'vimeo' && player && player.play) {
player.play();
} else if (playerType === 'html5' && player && player.play) {
player.play();
}
}
function renderTimeline() {
const container = document.getElementById('timelineMarkers');
container.innerHTML = '';
data.interactions.forEach((interaction, index) => {
const marker = document.createElement('div');
marker.className = 'timeline-marker';
if (completedInteractions.has(index)) {
marker.classList.add('completed');
}
const iconMap = {
question: '❓',
info: '',
link: '🔗'
};
marker.textContent = `${iconMap[interaction.type]} ${interaction.time} - ${interaction.title}`;
marker.onclick = () => seekTo(interaction.time);
container.appendChild(marker);
});
}
function seekTo(timeStr) {
const seconds = timeToSeconds(timeStr);
if (playerType === 'youtube' && player && player.seekTo) {
player.seekTo(seconds, true);
} else if (playerType === 'vimeo' && player && player.setCurrentTime) {
player.setCurrentTime(seconds);
} else if (playerType === 'html5' && player) {
player.currentTime = seconds;
}
}
function timeToSeconds(timeStr) {
const parts = timeStr.split(':');
const minutes = parseInt(parts[0]) || 0;
const seconds = parseInt(parts[1]) || 0;
return minutes * 60 + seconds;
}
loadContent();
</script>
</body>
</html>