This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/studio.py.monolithic.bak
Benjamin Admin bfdaf63ba9 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>
2026-02-09 09:51:32 +01:00

12525 lines
487 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
router = APIRouter()
@router.get("/app", response_class=HTMLResponse)
def app_ui():
return """
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>BreakPilot Arbeitsblatt Studio</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* ==========================================
DARK MODE (Default) - Original Design
========================================== */
:root {
--bp-primary: #0f766e;
--bp-primary-soft: #ccfbf1;
--bp-bg: #020617;
--bp-surface: #020617;
--bp-surface-elevated: rgba(15,23,42,0.9);
--bp-border: #1f2937;
--bp-border-subtle: rgba(148,163,184,0.25);
--bp-accent: #22c55e;
--bp-accent-soft: rgba(34,197,94,0.2);
--bp-text: #e5e7eb;
--bp-text-muted: #9ca3af;
--bp-danger: #ef4444;
--bp-gradient-bg: radial-gradient(circle at top left, #1f2937 0, #020617 50%, #000 100%);
--bp-gradient-surface: radial-gradient(circle at top left, rgba(15,23,42,0.9) 0, #020617 50%, #000 100%);
--bp-gradient-sidebar: radial-gradient(circle at top, #020617 0, #020617 40%, #000 100%);
--bp-gradient-topbar: linear-gradient(to right, rgba(15,23,42,0.9), rgba(15,23,42,0.6));
--bp-btn-primary-bg: linear-gradient(to right, var(--bp-primary), #15803d);
--bp-btn-primary-hover: linear-gradient(to right, #0f766e, #166534);
--bp-card-bg: var(--bp-gradient-surface);
--bp-input-bg: rgba(255,255,255,0.05);
--bp-scrollbar-track: rgba(15,23,42,0.5);
--bp-scrollbar-thumb: rgba(148,163,184,0.5);
}
/* ==========================================
LIGHT MODE - Brandbook Design
Farben aus BPAI Brandbook:
- Graphitgrau #4A4A4A (Klarheit, Struktur)
- Bordeauxrot #6C1B1B (Wärme, Bildung)
- Goldgelb #F1C40F (Zukunft, Energie)
- Grün #5ABF60 (Natur, Nachhaltigkeit)
- Weißgrau #F8F8F8 (Ruhe, Raum)
========================================== */
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap');
[data-theme="light"] {
--bp-primary: #6C1B1B;
--bp-primary-soft: rgba(108, 27, 27, 0.1);
--bp-bg: #F8F8F8;
--bp-surface: #FFFFFF;
--bp-surface-elevated: #FFFFFF;
--bp-border: #E0E0E0;
--bp-border-subtle: rgba(108, 27, 27, 0.15);
--bp-accent: #5ABF60;
--bp-accent-soft: rgba(90, 191, 96, 0.15);
--bp-text: #4A4A4A;
--bp-text-muted: #6B6B6B;
--bp-danger: #6C1B1B;
--bp-gradient-bg: linear-gradient(145deg, #F8F8F8 0%, #EFEFEF 100%);
--bp-gradient-surface: linear-gradient(145deg, #FFFFFF 0%, #F8F8F8 100%);
--bp-gradient-sidebar: linear-gradient(180deg, #FFFFFF 0%, #F5F5F5 100%);
--bp-gradient-topbar: linear-gradient(to right, #FFFFFF, #FAFAFA);
--bp-btn-primary-bg: linear-gradient(135deg, #6C1B1B 0%, #8B2323 100%);
--bp-btn-primary-hover: linear-gradient(135deg, #8B2323 0%, #A52A2A 100%);
--bp-card-bg: linear-gradient(145deg, #FFFFFF 0%, #F8F8F8 100%);
--bp-input-bg: #FFFFFF;
--bp-scrollbar-track: rgba(0,0,0,0.05);
--bp-scrollbar-thumb: rgba(108, 27, 27, 0.3);
--bp-gold: #F1C40F;
}
[data-theme="light"] body {
font-family: 'Manrope', system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bp-scrollbar-track);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--bp-scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--bp-scrollbar-thumb);
opacity: 0.8;
}
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
background: var(--bp-gradient-bg);
color: var(--bp-text);
min-height: 100vh;
display: flex;
flex-direction: column;
transition: background 0.3s ease, color 0.3s ease;
}
.app-root {
display: grid;
grid-template-rows: 56px 1fr 32px;
flex: 1;
min-height: 0;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid var(--bp-border-subtle);
backdrop-filter: blur(18px);
background: var(--bp-gradient-topbar);
transition: background 0.3s ease, border-color 0.3s ease;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo {
width: 28px;
height: 28px;
border-radius: 999px;
border: 2px solid var(--bp-accent);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
color: var(--bp-accent);
}
.brand-text-main {
font-weight: 600;
font-size: 16px;
}
.brand-text-sub {
font-size: 12px;
color: var(--bp-text-muted);
}
.top-nav {
display: flex;
align-items: center;
gap: 18px;
font-size: 13px;
}
.top-nav-item {
cursor: pointer;
padding: 4px 10px;
border-radius: 999px;
color: var(--bp-text-muted);
border: 1px solid transparent;
}
.top-nav-item.active {
color: var(--bp-primary);
border-color: var(--bp-accent-soft);
background: var(--bp-surface-elevated);
}
[data-theme="light"] .top-nav-item.active {
color: var(--bp-primary);
background: var(--bp-primary-soft);
border-color: var(--bp-primary);
}
.top-actions {
display: flex;
align-items: center;
gap: 10px;
}
.pill {
font-size: 11px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--bp-border-subtle);
color: var(--bp-text-muted);
}
/* Theme Toggle Button */
.theme-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
color: var(--bp-text-muted);
cursor: pointer;
font-size: 11px;
transition: all 0.2s ease;
}
.theme-toggle:hover {
border-color: var(--bp-primary);
color: var(--bp-primary);
}
.theme-toggle-icon {
font-size: 14px;
}
/* ==========================================
LEGAL MODAL STYLES
========================================== */
.legal-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
}
.legal-modal.active {
display: flex;
}
.legal-modal-content {
background: var(--bp-surface);
border-radius: 16px;
width: 90%;
max-width: 700px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 1px solid var(--bp-border);
}
.legal-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--bp-border);
}
.legal-modal-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.legal-modal-close {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: var(--bp-text-muted);
padding: 0;
line-height: 1;
}
.legal-modal-close:hover {
color: var(--bp-text);
}
.legal-tabs {
display: flex;
gap: 4px;
padding: 12px 24px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface-elevated);
}
.legal-tab {
padding: 8px 16px;
border: none;
background: transparent;
color: var(--bp-text-muted);
cursor: pointer;
border-radius: 8px;
font-size: 13px;
transition: all 0.2s ease;
}
.legal-tab:hover {
background: var(--bp-border-subtle);
color: var(--bp-text);
}
.legal-tab.active {
background: var(--bp-primary);
color: white;
}
.legal-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.legal-content {
display: none;
}
.legal-content.active {
display: block;
}
.legal-content h3 {
margin-top: 0;
color: var(--bp-text);
}
.legal-content p {
line-height: 1.6;
color: var(--bp-text-muted);
}
.legal-content ul {
color: var(--bp-text-muted);
line-height: 1.8;
}
.cookie-categories {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
}
.cookie-category {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: var(--bp-surface-elevated);
border-radius: 8px;
border: 1px solid var(--bp-border);
cursor: pointer;
}
.cookie-category input {
margin-top: 4px;
}
.cookie-category span {
flex: 1;
font-size: 13px;
color: var(--bp-text);
}
.gdpr-actions {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 16px;
}
.gdpr-action {
padding: 16px;
background: var(--bp-surface-elevated);
border-radius: 12px;
border: 1px solid var(--bp-border);
}
.gdpr-action h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: var(--bp-text);
}
.gdpr-action p {
margin: 0 0 12px 0;
font-size: 13px;
}
.btn-danger {
background: var(--bp-danger) !important;
border-color: var(--bp-danger) !important;
}
.btn-danger:hover {
filter: brightness(1.1);
}
[data-theme="light"] .legal-modal-content {
background: #FFFFFF;
border-color: #E0E0E0;
}
[data-theme="light"] .legal-tabs {
background: #F5F5F5;
}
[data-theme="light"] .legal-tab.active {
background: var(--bp-primary);
color: white;
}
[data-theme="light"] .cookie-category,
[data-theme="light"] .gdpr-action {
background: #F8F8F8;
border-color: #E0E0E0;
}
/* ==========================================
AUTH MODAL STYLES
========================================== */
.auth-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
}
.auth-modal.active {
display: flex;
}
.auth-modal-content {
background: var(--bp-surface);
border-radius: 16px;
width: 90%;
max-width: 420px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 1px solid var(--bp-border);
}
.auth-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--bp-border);
}
.auth-modal-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.auth-tabs {
display: flex;
gap: 4px;
padding: 12px 24px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface-elevated);
}
.auth-tab {
flex: 1;
padding: 10px 16px;
border: none;
background: transparent;
color: var(--bp-text-muted);
cursor: pointer;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.auth-tab:hover {
background: var(--bp-border-subtle);
color: var(--bp-text);
}
.auth-tab.active {
background: var(--bp-primary);
color: white;
}
.auth-body {
padding: 24px;
}
.auth-content {
display: none;
}
.auth-content.active {
display: block;
}
.auth-form-group {
margin-bottom: 16px;
}
.auth-form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: var(--bp-text);
}
.auth-form-input {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--bp-border);
border-radius: 8px;
background: var(--bp-surface-elevated);
color: var(--bp-text);
font-size: 14px;
transition: border-color 0.2s ease;
box-sizing: border-box;
}
.auth-form-input:focus {
outline: none;
border-color: var(--bp-primary);
}
.auth-form-input::placeholder {
color: var(--bp-text-muted);
}
.auth-error {
display: none;
padding: 10px 14px;
background: rgba(220, 53, 69, 0.15);
border: 1px solid var(--bp-danger);
border-radius: 8px;
color: var(--bp-danger);
font-size: 13px;
margin-bottom: 16px;
}
.auth-error.active {
display: block;
}
.auth-success {
display: none;
padding: 10px 14px;
background: rgba(40, 167, 69, 0.15);
border: 1px solid var(--bp-success);
border-radius: 8px;
color: var(--bp-success);
font-size: 13px;
margin-bottom: 16px;
}
.auth-success.active {
display: block;
}
.auth-btn {
width: 100%;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.auth-btn-primary {
background: var(--bp-primary);
color: white;
}
.auth-btn-primary:hover {
filter: brightness(1.1);
}
.auth-btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.auth-link {
text-align: center;
margin-top: 16px;
font-size: 13px;
color: var(--bp-text-muted);
}
.auth-link a {
color: var(--bp-primary);
text-decoration: none;
cursor: pointer;
}
.auth-link a:hover {
text-decoration: underline;
}
.auth-divider {
display: flex;
align-items: center;
text-align: center;
margin: 20px 0;
color: var(--bp-text-muted);
font-size: 12px;
}
.auth-divider::before,
.auth-divider::after {
content: '';
flex: 1;
border-bottom: 1px solid var(--bp-border);
}
.auth-divider::before {
margin-right: 12px;
}
.auth-divider::after {
margin-left: 12px;
}
.auth-user-dropdown {
position: relative;
display: none;
}
.auth-user-dropdown.active {
display: flex;
}
.auth-user-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 8px;
color: var(--bp-text);
cursor: pointer;
font-size: 13px;
}
.auth-user-btn:hover {
border-color: var(--bp-primary);
}
.auth-user-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--bp-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.auth-user-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
min-width: 200px;
z-index: 1000;
overflow: hidden;
}
.auth-user-menu.active {
display: block;
}
.auth-user-menu-header {
padding: 12px 16px;
border-bottom: 1px solid var(--bp-border);
}
.auth-user-menu-name {
font-weight: 600;
font-size: 14px;
color: var(--bp-text);
}
.auth-user-menu-email {
font-size: 12px;
color: var(--bp-text-muted);
margin-top: 2px;
}
.auth-user-menu-item {
display: block;
width: 100%;
padding: 10px 16px;
border: none;
background: none;
text-align: left;
color: var(--bp-text);
cursor: pointer;
font-size: 13px;
transition: background 0.2s ease;
}
.auth-user-menu-item:hover {
background: var(--bp-surface-elevated);
}
.auth-user-menu-item.danger {
color: var(--bp-danger);
}
[data-theme="light"] .auth-modal-content {
background: #FFFFFF;
border-color: #E0E0E0;
}
[data-theme="light"] .auth-tabs {
background: #F5F5F5;
}
[data-theme="light"] .auth-form-input {
background: #FFFFFF;
border-color: #E0E0E0;
}
[data-theme="light"] .auth-user-menu {
background: #FFFFFF;
border-color: #E0E0E0;
}
/* ==========================================
NOTIFICATION STYLES
========================================== */
.notification-bell {
position: relative;
display: none;
}
.notification-bell.active {
display: flex;
}
.notification-bell-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 8px;
color: var(--bp-text-muted);
cursor: pointer;
font-size: 18px;
transition: all 0.2s ease;
}
.notification-bell-btn:hover {
border-color: var(--bp-primary);
color: var(--bp-primary);
}
.notification-badge {
position: absolute;
top: 2px;
right: 2px;
min-width: 18px;
height: 18px;
background: var(--bp-danger);
color: white;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
padding: 0 5px;
}
.notification-badge.hidden {
display: none;
}
.notification-panel {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
width: 360px;
max-height: 480px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
z-index: 1000;
overflow: hidden;
}
.notification-panel.active {
display: flex;
flex-direction: column;
}
.notification-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--bp-border);
}
.notification-panel-title {
font-weight: 600;
font-size: 16px;
color: var(--bp-text);
}
.notification-panel-actions {
display: flex;
gap: 8px;
}
.notification-panel-action {
padding: 4px 8px;
background: none;
border: 1px solid var(--bp-border);
border-radius: 6px;
color: var(--bp-text-muted);
cursor: pointer;
font-size: 11px;
transition: all 0.2s ease;
}
.notification-panel-action:hover {
border-color: var(--bp-primary);
color: var(--bp-primary);
}
.notification-list {
flex: 1;
overflow-y: auto;
max-height: 380px;
}
.notification-item {
display: flex;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--bp-border);
cursor: pointer;
transition: background 0.2s ease;
}
.notification-item:hover {
background: var(--bp-surface-elevated);
}
.notification-item.unread {
background: rgba(15, 118, 110, 0.08);
}
.notification-item.unread::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--bp-primary);
}
.notification-item {
position: relative;
}
.notification-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--bp-primary-soft);
color: var(--bp-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-title {
font-weight: 500;
font-size: 13px;
color: var(--bp-text);
margin-bottom: 4px;
line-height: 1.3;
}
.notification-body {
font-size: 12px;
color: var(--bp-text-muted);
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.notification-time {
font-size: 11px;
color: var(--bp-text-muted);
margin-top: 6px;
}
.notification-empty {
padding: 40px 20px;
text-align: center;
color: var(--bp-text-muted);
}
.notification-empty-icon {
font-size: 36px;
margin-bottom: 12px;
opacity: 0.5;
}
.notification-footer {
padding: 12px 16px;
border-top: 1px solid var(--bp-border);
text-align: center;
}
.notification-footer-btn {
background: none;
border: none;
color: var(--bp-primary);
cursor: pointer;
font-size: 13px;
font-weight: 500;
}
.notification-footer-btn:hover {
text-decoration: underline;
}
/* Notification Preferences Modal */
.notification-prefs-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10001;
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
}
.notification-prefs-modal.active {
display: flex;
}
.notification-prefs-content {
background: var(--bp-surface);
border-radius: 16px;
width: 90%;
max-width: 420px;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 1px solid var(--bp-border);
}
.notification-pref-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid var(--bp-border);
}
.notification-pref-item:last-child {
border-bottom: none;
}
.notification-pref-label {
font-size: 14px;
color: var(--bp-text);
}
.notification-pref-desc {
font-size: 12px;
color: var(--bp-text-muted);
margin-top: 2px;
}
.toggle-switch {
position: relative;
width: 48px;
height: 26px;
background: var(--bp-border);
border-radius: 999px;
cursor: pointer;
transition: background 0.3s ease;
}
.toggle-switch.active {
background: var(--bp-primary);
}
.toggle-switch-handle {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 0.3s ease;
}
.toggle-switch.active .toggle-switch-handle {
transform: translateX(22px);
}
[data-theme="light"] .notification-panel {
background: #FFFFFF;
border-color: #E0E0E0;
}
[data-theme="light"] .notification-item.unread {
background: rgba(108, 27, 27, 0.05);
}
[data-theme="light"] .notification-item.unread::before {
background: var(--bp-primary);
}
/* ==========================================
SUSPENSION OVERLAY STYLES
========================================== */
.suspension-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 20000;
justify-content: center;
align-items: center;
backdrop-filter: blur(8px);
}
.suspension-overlay.active {
display: flex;
}
.suspension-content {
background: var(--bp-surface);
border-radius: 20px;
width: 90%;
max-width: 500px;
padding: 40px;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
border: 1px solid var(--bp-danger);
}
.suspension-icon {
font-size: 64px;
margin-bottom: 20px;
}
.suspension-title {
font-size: 24px;
font-weight: 700;
color: var(--bp-danger);
margin-bottom: 16px;
}
.suspension-message {
font-size: 15px;
color: var(--bp-text-muted);
margin-bottom: 24px;
line-height: 1.6;
}
.suspension-docs {
background: var(--bp-surface-elevated);
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
text-align: left;
}
.suspension-docs-title {
font-size: 13px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 12px;
}
.suspension-doc-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: var(--bp-bg);
border-radius: 8px;
margin-bottom: 8px;
border: 1px solid var(--bp-border);
}
.suspension-doc-item:last-child {
margin-bottom: 0;
}
.suspension-doc-name {
font-size: 14px;
color: var(--bp-text);
}
.suspension-doc-deadline {
font-size: 12px;
color: var(--bp-danger);
font-weight: 500;
}
.suspension-btn {
padding: 14px 28px;
background: var(--bp-btn-primary-bg);
color: white;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.suspension-btn:hover {
background: var(--bp-btn-primary-hover);
transform: translateY(-1px);
}
.suspension-countdown {
font-size: 12px;
color: var(--bp-text-muted);
margin-top: 16px;
}
/* ==========================================
ADMIN PANEL STYLES
========================================== */
.admin-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
}
.admin-modal.active {
display: flex;
}
.admin-modal-content {
background: var(--bp-surface);
border-radius: 16px;
width: 95%;
max-width: 1000px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
border: 1px solid var(--bp-border);
}
.admin-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--bp-border);
}
.admin-modal-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.admin-tabs {
display: flex;
gap: 4px;
padding: 12px 24px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface-elevated);
}
.admin-tab {
padding: 8px 16px;
border: none;
background: transparent;
color: var(--bp-text-muted);
cursor: pointer;
border-radius: 8px;
font-size: 13px;
transition: all 0.2s ease;
}
.admin-tab:hover {
background: var(--bp-border-subtle);
color: var(--bp-text);
}
.admin-tab.active {
background: var(--bp-primary);
color: white;
}
.admin-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.admin-content {
display: none;
}
.admin-content.active {
display: block;
}
.admin-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 12px;
}
.admin-toolbar-left {
display: flex;
gap: 12px;
align-items: center;
}
.admin-search {
padding: 8px 12px;
border: 1px solid var(--bp-border);
border-radius: 8px;
background: var(--bp-surface-elevated);
color: var(--bp-text);
width: 250px;
}
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.admin-table th,
.admin-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--bp-border);
}
.admin-table th {
background: var(--bp-surface-elevated);
font-weight: 600;
color: var(--bp-text);
}
.admin-table tr:hover {
background: var(--bp-surface-elevated);
}
.admin-table td {
color: var(--bp-text-muted);
}
.admin-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.admin-badge-published {
background: rgba(74, 222, 128, 0.2);
color: #4ADE80;
}
.admin-badge-draft {
background: rgba(251, 191, 36, 0.2);
color: #FBBF24;
}
.admin-badge-archived {
background: rgba(156, 163, 175, 0.2);
color: #9CA3AF;
}
.admin-badge-rejected {
background: rgba(239, 68, 68, 0.2);
color: #EF4444;
}
.admin-badge-review {
background: rgba(147, 51, 234, 0.2);
color: #A855F7;
}
.admin-badge-approved {
background: rgba(34, 197, 94, 0.2);
color: #22C55E;
}
.admin-badge-submitted {
background: rgba(59, 130, 246, 0.2);
color: #3B82F6;
}
.admin-badge-rejected {
background: rgba(239, 68, 68, 0.2);
color: #EF4444;
}
.admin-badge-mandatory {
background: rgba(239, 68, 68, 0.2);
color: #EF4444;
}
.admin-badge-optional {
background: rgba(96, 165, 250, 0.2);
color: #60A5FA;
}
.admin-actions {
display: flex;
gap: 8px;
}
.admin-btn {
padding: 6px 10px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.admin-btn-edit {
background: var(--bp-primary);
color: white;
}
.admin-btn-edit:hover {
filter: brightness(1.1);
}
.admin-btn-delete {
background: rgba(239, 68, 68, 0.2);
color: #EF4444;
}
.admin-btn-delete:hover {
background: rgba(239, 68, 68, 0.3);
}
.admin-btn-publish {
background: rgba(74, 222, 128, 0.2);
color: #4ADE80;
}
.admin-btn-publish:hover {
background: rgba(74, 222, 128, 0.3);
}
.admin-form {
display: none;
background: var(--bp-surface-elevated);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid var(--bp-border);
}
.admin-form.active {
display: block;
}
/* Info Text in Toolbar */
.admin-info-text {
font-size: 12px;
color: var(--bp-text-muted);
font-style: italic;
}
/* Dialog Overlay */
.admin-dialog {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.admin-dialog.active {
display: flex;
}
.admin-dialog-content {
background: var(--bp-surface);
border-radius: 12px;
padding: 24px;
max-width: 500px;
width: 90%;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.admin-dialog-content h3 {
margin: 0 0 12px 0;
font-size: 18px;
}
.admin-dialog-info {
font-size: 13px;
color: var(--bp-text-muted);
margin-bottom: 20px;
line-height: 1.5;
}
.admin-dialog-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 20px;
}
/* Scheduled Badge */
.admin-badge-scheduled {
background: #f59e0b;
color: white;
}
/* Version Compare Overlay */
.version-compare-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--bp-bg);
z-index: 2000;
flex-direction: column;
}
.version-compare-overlay.active {
display: flex;
}
.version-compare-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: var(--bp-surface);
border-bottom: 1px solid var(--bp-border);
}
.version-compare-header h2 {
margin: 0;
font-size: 18px;
}
.version-compare-info {
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
}
.compare-vs {
color: var(--bp-text-muted);
font-weight: 600;
}
.version-compare-container {
display: grid;
grid-template-columns: 1fr 1fr;
flex: 1;
overflow: hidden;
gap: 0;
}
.version-compare-panel {
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--bp-border);
}
.version-compare-panel:last-child {
border-right: none;
}
.version-compare-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--bp-surface);
border-bottom: 1px solid var(--bp-border);
}
.compare-label {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.compare-label-published {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
.compare-label-draft {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.version-compare-content {
flex: 1;
overflow-y: auto;
padding: 24px;
font-size: 14px;
line-height: 1.7;
}
.version-compare-content h1,
.version-compare-content h2,
.version-compare-content h3 {
margin-top: 24px;
margin-bottom: 12px;
}
.version-compare-content p {
margin-bottom: 12px;
}
.version-compare-content ul,
.version-compare-content ol {
margin-bottom: 12px;
padding-left: 24px;
}
.version-compare-content .no-content {
color: var(--bp-text-muted);
font-style: italic;
text-align: center;
padding: 40px;
}
.version-compare-footer {
padding: 12px 24px;
background: var(--bp-surface);
border-top: 1px solid var(--bp-border);
max-height: 150px;
overflow-y: auto;
}
.compare-history-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
color: var(--bp-text-muted);
}
.compare-history-item {
font-size: 12px;
padding: 4px 0;
border-bottom: 1px solid var(--bp-border-subtle);
}
.compare-history-item:last-child {
border-bottom: none;
}
.compare-history-list {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
font-size: 12px;
color: var(--bp-text-muted);
}
.admin-form-title {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
}
.admin-form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.admin-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.admin-form-group.full-width {
grid-column: 1 / -1;
}
.admin-form-label {
font-size: 12px;
font-weight: 600;
color: var(--bp-text);
}
.admin-form-input,
.admin-form-select,
.admin-form-textarea {
padding: 10px 12px;
border: 1px solid var(--bp-border);
border-radius: 8px;
background: var(--bp-surface);
color: var(--bp-text);
font-size: 13px;
}
.admin-form-textarea {
min-height: 150px;
resize: vertical;
font-family: inherit;
}
.admin-form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--bp-border);
}
.admin-empty {
text-align: center;
padding: 40px;
color: var(--bp-text-muted);
}
.admin-loading {
text-align: center;
padding: 40px;
color: var(--bp-text-muted);
}
[data-theme="light"] .admin-modal-content {
background: #FFFFFF;
border-color: #E0E0E0;
}
[data-theme="light"] .admin-tabs {
background: #F5F5F5;
}
[data-theme="light"] .admin-table th {
background: #F5F5F5;
}
[data-theme="light"] .admin-form {
background: #F8F8F8;
border-color: #E0E0E0;
}
/* DSMS Styles */
.dsms-subtab {
padding: 6px 14px;
border: none;
background: transparent;
color: var(--bp-text-muted);
cursor: pointer;
font-size: 13px;
border-radius: 4px;
transition: all 0.2s ease;
}
.dsms-subtab:hover {
background: var(--bp-border-subtle);
color: var(--bp-text);
}
.dsms-subtab.active {
background: var(--bp-primary);
color: white;
}
.dsms-content {
display: none;
}
.dsms-content.active {
display: block;
}
.dsms-status-card {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 8px;
padding: 16px;
}
.dsms-status-card h4 {
margin: 0 0 8px 0;
font-size: 12px;
color: var(--bp-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.dsms-status-card .value {
font-size: 24px;
font-weight: 600;
color: var(--bp-text);
}
.dsms-status-card .value.online {
color: var(--bp-accent);
}
.dsms-status-card .value.offline {
color: var(--bp-danger);
}
.dsms-verify-success {
background: var(--bp-accent-soft);
border: 1px solid var(--bp-accent);
border-radius: 8px;
padding: 16px;
color: var(--bp-accent);
}
.dsms-verify-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--bp-danger);
border-radius: 8px;
padding: 16px;
color: var(--bp-danger);
}
/* DSMS WebUI Styles */
.dsms-webui-nav {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: none;
background: transparent;
color: var(--bp-text-muted);
font-size: 14px;
border-radius: 6px;
cursor: pointer;
text-align: left;
width: 100%;
transition: all 0.2s;
}
.dsms-webui-nav:hover {
background: var(--bp-surface-elevated);
color: var(--bp-text);
}
.dsms-webui-nav.active {
background: var(--bp-primary-soft);
color: var(--bp-primary);
font-weight: 500;
}
.dsms-webui-section {
display: none;
}
.dsms-webui-section.active {
display: block;
}
.dsms-webui-stat-card {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 8px;
padding: 16px;
}
.dsms-webui-stat-label {
font-size: 12px;
color: var(--bp-text-muted);
margin-bottom: 4px;
}
.dsms-webui-stat-value {
font-size: 18px;
font-weight: 600;
color: var(--bp-text);
}
.dsms-webui-stat-sub {
font-size: 11px;
color: var(--bp-text-muted);
margin-top: 4px;
}
.dsms-webui-upload-zone {
border: 2px dashed var(--bp-border);
border-radius: 12px;
padding: 48px 24px;
background: var(--bp-input-bg);
transition: all 0.2s;
}
.dsms-webui-upload-zone.dragover {
border-color: var(--bp-primary);
background: var(--bp-primary-soft);
}
.dsms-webui-file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 8px;
margin-bottom: 8px;
}
.dsms-webui-file-item .cid {
font-family: monospace;
font-size: 12px;
color: var(--bp-text-muted);
word-break: break-all;
}
.main-layout {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
height: 100%;
min-height: 0;
}
.sidebar {
border-right: 1px solid var(--bp-border-subtle);
background: var(--bp-gradient-sidebar);
padding: 14px 10px;
display: flex;
flex-direction: column;
gap: 18px;
min-width: 0;
height: 100%;
max-height: 100vh;
overflow-y: auto;
overflow-x: hidden;
transition: background 0.3s ease, border-color 0.3s ease;
}
.sidebar-section-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--bp-text);
padding: 8px 6px 6px 6px;
margin-top: 12px;
}
.sidebar-menu {
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border-radius: 9px;
cursor: pointer;
font-size: 13px;
color: var(--bp-text-muted);
}
.sidebar-item.active {
background: var(--bp-surface-elevated);
color: var(--bp-accent);
border: 1px solid var(--bp-accent-soft);
}
[data-theme="light"] .sidebar-item.active {
background: var(--bp-primary-soft);
color: var(--bp-primary);
border: 1px solid var(--bp-primary);
}
.sidebar-item-label {
display: flex;
align-items: center;
gap: 7px;
}
.sidebar-item-badge {
font-size: 10px;
border-radius: 999px;
padding: 2px 7px;
border: 1px solid var(--bp-border-subtle);
}
.sidebar-footer {
margin-top: auto;
font-size: 11px;
color: var(--bp-text-muted);
padding: 0 6px;
}
/* vast.ai GPU Control */
.vast-control {
padding: 8px;
font-size: 12px;
}
.vast-status-row {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.vast-label {
color: var(--bp-text-muted);
}
.vast-value {
color: var(--bp-text);
font-weight: 500;
}
.vast-badge {
padding: 2px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.vast-badge-running {
background: var(--bp-accent-soft);
color: var(--bp-accent);
}
.vast-badge-stopped {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.vast-badge-loading {
background: rgba(234, 179, 8, 0.2);
color: #eab308;
}
.vast-badge-unknown {
background: rgba(148, 163, 184, 0.2);
color: var(--bp-text-muted);
}
.vast-buttons {
display: flex;
gap: 8px;
margin-top: 10px;
}
.vast-buttons .btn {
flex: 1;
}
.vast-message {
margin-top: 8px;
font-size: 11px;
color: var(--bp-text-muted);
min-height: 16px;
}
.vast-message.error {
color: var(--bp-danger);
}
.vast-message.success {
color: var(--bp-accent);
}
.content {
padding: 14px 16px 16px 16px;
display: flex;
flex-direction: column;
gap: 14px;
height: 100%;
min-height: 0;
overflow: hidden;
}
.panel {
background: var(--bp-gradient-surface);
border-radius: 16px;
border: 1px solid var(--bp-border-subtle);
padding: 12px 14px;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
flex: 1;
overflow: hidden;
transition: background 0.3s ease, border-color 0.3s ease;
}
[data-theme="light"] .panel {
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.panel-title {
font-size: 18px;
font-weight: 700;
color: var(--bp-text);
margin-bottom: 4px;
}
.panel-subtitle {
font-size: 12px;
color: var(--bp-text-muted);
line-height: 1.5;
}
.panel-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
overflow: auto;
}
.small-pill {
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--bp-border-subtle);
color: var(--bp-text);
background: var(--bp-surface-elevated);
}
.upload-inline {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
background: var(--bp-surface-elevated);
border-radius: 8px;
border: 1px solid var(--bp-border-subtle);
}
[data-theme="light"] .upload-inline {
background: var(--bp-surface);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
}
.upload-inline input[type=file] {
font-size: 11px;
padding: 6px;
background: var(--bp-input-bg);
border: 1px solid var(--bp-border-subtle);
border-radius: 6px;
color: var(--bp-text);
cursor: pointer;
}
.upload-inline input[type=file]::file-selector-button {
background: var(--bp-accent-soft);
border: 1px solid var(--bp-accent);
border-radius: 4px;
padding: 4px 10px;
color: var(--bp-accent);
font-size: 10px;
font-weight: 600;
cursor: pointer;
margin-right: 8px;
}
[data-theme="light"] .upload-inline input[type=file]::file-selector-button {
background: var(--bp-primary-soft);
border-color: var(--bp-primary);
color: var(--bp-primary);
}
.upload-inline input[type=file]::file-selector-button:hover {
opacity: 0.8;
}
.file-list {
list-style: none;
margin: 0;
padding: 0;
max-height: 150px;
overflow-y: auto;
border-radius: 10px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
}
[data-theme="light"] .file-list {
border-color: var(--bp-primary);
background: var(--bp-surface);
}
.file-item {
font-size: 12px;
padding: 8px 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
transition: background 0.15s ease;
}
.file-item:nth-child(odd) {
background: var(--bp-surface-elevated);
}
[data-theme="light"] .file-item:nth-child(odd) {
background: rgba(108, 27, 27, 0.03);
}
.file-item:hover {
background: var(--bp-accent-soft);
}
[data-theme="light"] .file-item:hover {
background: var(--bp-primary-soft);
}
.file-item.active {
background: var(--bp-accent-soft);
}
.file-item-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.file-item-delete {
font-size: 14px;
color: #f97316;
cursor: pointer;
padding: 4px 6px;
border-radius: 4px;
transition: all 0.15s ease;
flex-shrink: 0;
}
.file-item-delete:hover {
color: #fb923c;
background: rgba(249,115,22,0.15);
}
.file-empty {
font-size: 12px;
color: var(--bp-text-muted);
}
.inline-process {
display: flex;
justify-content: flex-end;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(148,163,184,0.2);
}
.preview-container {
flex: 1;
border-radius: 12px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-gradient-sidebar);
overflow: hidden;
display: flex;
align-items: stretch;
justify-content: center;
position: relative;
min-height: 750px;
transition: all 0.3s ease;
}
[data-theme="light"] .preview-container {
background: var(--bp-bg);
border: 2px solid var(--bp-primary);
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
}
.preview-placeholder {
font-size: 13px;
color: var(--bp-text-muted);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px;
}
.compare-wrapper {
display: grid;
grid-template-columns: minmax(0, 1fr) 110px minmax(0, 1fr);
gap: 8px;
width: 100%;
height: 100%;
position: relative;
}
.compare-section {
position: relative;
border-radius: 10px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-gradient-sidebar);
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
transition: all 0.3s ease;
}
[data-theme="light"] .compare-section {
border: 2px solid var(--bp-primary);
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
}
.compare-header {
padding: 6px 10px;
font-size: 12px;
border-bottom: 1px solid var(--bp-border-subtle);
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
}
[data-theme="light"] .compare-header {
background: var(--bp-primary-soft);
border-bottom: 1px solid var(--bp-primary);
}
.compare-header span {
color: var(--bp-text-muted);
}
[data-theme="light"] .compare-header span {
color: var(--bp-primary);
font-weight: 600;
}
.compare-body {
flex: 1;
min-height: 0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 6px;
}
.compare-body-inner {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
box-shadow: 0 18px 40px rgba(0,0,0,0.5);
border-radius: 10px;
}
[data-theme="light"] .preview-img {
box-shadow: 0 8px 24px rgba(108, 27, 27, 0.15);
}
.clean-frame {
width: 100%;
height: 100%;
border: none;
border-radius: 10px;
background: white;
}
.preview-nav {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
pointer-events: none;
padding: 0 4px;
}
.preview-nav button {
pointer-events: auto;
width: 28px;
height: 28px;
border-radius: 999px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
color: var(--bp-text);
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
[data-theme="light"] .preview-nav button {
border: 2px solid var(--bp-primary);
background: var(--bp-surface);
color: var(--bp-primary);
font-weight: 700;
}
.preview-nav button:hover:not(:disabled) {
border-color: var(--bp-primary);
color: var(--bp-primary);
}
[data-theme="light"] .preview-nav button:hover:not(:disabled) {
background: var(--bp-primary);
color: white;
}
.preview-nav button:disabled {
opacity: 0.35;
cursor: default;
}
.preview-nav span {
position: absolute;
bottom: 6px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border-subtle);
}
[data-theme="light"] .preview-nav span {
background: var(--bp-surface);
border: 1px solid var(--bp-primary);
color: var(--bp-primary);
}
.preview-thumbnails {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 4px;
overflow-y: auto;
align-items: center;
}
.preview-thumb {
min-width: 90px;
width: 90px;
height: 70px;
border-radius: 8px;
border: 2px solid rgba(148,163,184,0.25);
background: rgba(15,23,42,0.5);
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
}
.preview-thumb:hover {
border-color: var(--bp-accent);
}
.preview-thumb.active {
border-color: var(--bp-accent);
border-width: 3px;
}
.preview-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-thumb-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
font-size: 9px;
padding: 2px;
background: rgba(0,0,0,0.8);
color: white;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pager {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 2px 0;
font-size: 11px;
}
.pager button {
width: 24px;
height: 24px;
border-radius: 999px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
color: var(--bp-text);
cursor: pointer;
transition: all 0.2s ease;
}
.pager button:hover {
border-color: var(--bp-primary);
color: var(--bp-primary);
}
[data-theme="light"] .pager button {
border: 2px solid var(--bp-primary);
background: var(--bp-surface);
color: var(--bp-primary);
font-weight: 700;
}
[data-theme="light"] .pager button:hover {
background: var(--bp-primary);
color: white;
}
.status-bar {
position: fixed;
right: 18px;
bottom: 18px;
padding: 8px 12px;
border-radius: 999px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border-subtle);
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
min-width: 230px;
transition: all 0.3s ease;
}
[data-theme="light"] .status-bar {
background: var(--bp-surface);
border: 2px solid var(--bp-primary);
box-shadow: 0 4px 16px rgba(108, 27, 27, 0.15);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--bp-text-muted);
}
.status-dot.busy {
background: var(--bp-accent);
}
.status-dot.error {
background: var(--bp-danger);
}
.status-text-main {
font-size: 12px;
}
.status-text-sub {
font-size: 11px;
color: var(--bp-text-muted);
}
.footer {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 11px;
color: var(--bp-text-muted);
border-top: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
transition: background 0.3s ease;
}
.footer a {
color: var(--bp-text-muted);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.btn {
font-size: 13px;
font-weight: 500;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
color: var(--bp-text);
cursor: pointer;
transition: all 0.2s ease;
}
.btn:hover:not(:disabled) {
border-color: var(--bp-primary);
transform: translateY(-1px);
}
.btn-primary {
border-color: var(--bp-accent);
background: var(--bp-btn-primary-bg);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--bp-btn-primary-hover);
box-shadow: 0 4px 12px rgba(34,197,94,0.3);
}
[data-theme="light"] .btn-primary:hover:not(:disabled) {
box-shadow: 0 4px 12px rgba(108, 27, 27, 0.3);
}
.btn-ghost {
background: transparent;
}
.btn-ghost:hover:not(:disabled) {
background: var(--bp-surface-elevated);
}
[data-theme="light"] .btn-ghost {
border-color: var(--bp-primary);
color: var(--bp-primary);
}
[data-theme="light"] .btn-ghost:hover:not(:disabled) {
background: var(--bp-primary-soft);
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.panel-tools-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.card-toggle-bar {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.toggle-pill {
font-size: 11px;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
color: var(--bp-text-muted);
cursor: pointer;
transition: all 0.2s ease;
}
.toggle-pill.active {
border-color: var(--bp-accent);
color: var(--bp-accent);
background: var(--bp-accent-soft);
}
[data-theme="light"] .toggle-pill.active {
border-color: var(--bp-primary);
color: var(--bp-primary);
background: var(--bp-primary-soft);
}
.cards-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: 1fr;
gap: 10px;
min-height: 0;
align-items: stretch;
}
.card {
border-radius: 14px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-card-bg);
padding: 10px;
display: flex;
flex-direction: column;
cursor: pointer;
min-height: 0;
transition: all 0.3s ease;
}
[data-theme="light"] .card {
border: 2px solid var(--bp-primary);
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
}
[data-theme="light"] .card:hover {
box-shadow: 0 6px 28px rgba(108, 27, 27, 0.2);
transform: translateY(-2px);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
[data-theme="light"] .card-header {
border-bottom: 1px solid var(--bp-border-subtle);
padding-bottom: 8px;
margin-bottom: 10px;
}
.card-title {
font-size: 13px;
font-weight: 500;
}
[data-theme="light"] .card-title {
font-size: 15px;
font-weight: 700;
color: var(--bp-text);
letter-spacing: -0.02em;
}
.card-badge {
font-size: 10px;
border-radius: 999px;
padding: 2px 7px;
border: 1px solid var(--bp-border-subtle);
color: var(--bp-text-muted);
}
[data-theme="light"] .card-badge {
background: linear-gradient(135deg, var(--bp-accent) 0%, #4CAF50 100%);
color: white;
border: none;
font-weight: 600;
padding: 4px 10px;
}
.card-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 5px;
font-size: 11px;
color: var(--bp-text-muted);
}
[data-theme="light"] .card-body {
font-size: 12px;
line-height: 1.6;
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.card-hidden {
display: none;
}
.card-full {
grid-column: 1 / -1;
min-height: 220px;
}
/* Vollbild-Overlay für Doppelklick */
.lightbox {
position: fixed;
inset: 0;
background: rgba(15,23,42,0.96);
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
}
.lightbox.hidden {
display: none;
}
.lightbox-inner {
position: relative;
max-width: 96vw;
max-height: 96vh;
}
.lightbox-img {
max-width: 96vw;
max-height: 96vh;
object-fit: contain;
border-radius: 12px;
box-shadow: 0 20px 45px rgba(0,0,0,0.7);
}
.lightbox-close {
position: absolute;
top: -32px;
right: 0;
font-size: 11px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,0.6);
background: rgba(15,23,42,0.9);
color: var(--bp-text);
cursor: pointer;
}
.lightbox-caption {
margin-top: 6px;
font-size: 11px;
color: var(--bp-text-muted);
text-align: center;
}
/* Lerneinheiten-Projektleiste im Sidebar */
.unit-form {
margin-top: 12px;
padding: 12px 10px;
border-radius: 10px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border-subtle);
display: flex;
flex-direction: column;
gap: 8px;
transition: all 0.3s ease;
}
[data-theme="light"] .unit-form {
background: var(--bp-surface);
border: 2px solid var(--bp-primary);
box-shadow: 0 4px 16px rgba(108, 27, 27, 0.1);
}
.unit-form-row {
display: flex;
gap: 6px;
}
.unit-input {
flex: 1;
border-radius: 8px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-bg);
color: var(--bp-text);
font-size: 12px;
padding: 8px 10px;
transition: border-color 0.2s ease;
}
[data-theme="light"] .unit-input {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
}
.unit-input:focus {
outline: none;
border-color: var(--bp-primary);
}
.unit-input::placeholder {
color: var(--bp-text-muted);
}
.unit-list {
margin: 8px 0 0 0;
padding: 0 2px;
max-height: 160px;
overflow-y: auto;
list-style: none;
}
.unit-item {
font-size: 12px;
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
color: var(--bp-text);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
transition: all 0.15s ease;
margin-bottom: 4px;
}
.unit-item:hover {
background: var(--bp-surface-elevated);
}
[data-theme="light"] .unit-item:hover {
background: var(--bp-primary-soft);
}
.unit-item.active {
background: var(--bp-surface-elevated);
color: var(--bp-accent);
border: 1px solid var(--bp-accent);
font-weight: 600;
}
[data-theme="light"] .unit-item.active {
background: var(--bp-primary-soft);
color: var(--bp-primary);
border: 2px solid var(--bp-primary);
}
.unit-item-meta {
font-size: 10px;
color: var(--bp-text-muted);
margin-top: 2px;
}
.btn-unit-add {
align-self: flex-end;
}
/* MC Preview Styles */
.mc-preview {
margin-top: 8px;
max-height: 300px;
overflow-y: auto;
}
.mc-question {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border-subtle);
border-radius: 8px;
padding: 10px;
margin-bottom: 8px;
transition: all 0.3s ease;
}
[data-theme="light"] .mc-question {
background: var(--bp-surface);
border: 1px solid var(--bp-primary);
box-shadow: 0 2px 8px rgba(108, 27, 27, 0.08);
}
.mc-question-text {
font-size: 12px;
font-weight: 500;
margin-bottom: 8px;
color: var(--bp-text);
}
.mc-options {
display: flex;
flex-direction: column;
gap: 4px;
}
.mc-option {
font-size: 11px;
padding: 6px 10px;
border-radius: 6px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border-subtle);
cursor: pointer;
transition: all 0.15s ease;
}
[data-theme="light"] .mc-option {
background: var(--bp-bg);
border: 1px solid var(--bp-border);
}
.mc-option:hover {
background: var(--bp-accent-soft);
border-color: var(--bp-accent);
}
[data-theme="light"] .mc-option:hover {
background: var(--bp-primary-soft);
border-color: var(--bp-primary);
}
.mc-option.selected {
background: var(--bp-accent-soft);
border-color: var(--bp-accent);
}
[data-theme="light"] .mc-option.selected {
background: var(--bp-primary-soft);
border-color: var(--bp-primary);
}
.mc-option.correct {
background: rgba(90, 191, 96, 0.2);
border-color: var(--bp-accent);
}
.mc-option.incorrect {
background: rgba(108, 27, 27, 0.2);
border-color: rgba(239,68,68,0.6);
}
.mc-option-label {
font-weight: 600;
margin-right: 6px;
text-transform: uppercase;
}
.mc-feedback {
margin-top: 8px;
font-size: 11px;
padding: 6px 8px;
border-radius: 6px;
background: rgba(34,197,94,0.1);
border: 1px solid rgba(34,197,94,0.3);
color: var(--bp-accent);
}
.mc-stats {
display: flex;
gap: 12px;
margin-top: 8px;
padding: 8px;
background: var(--bp-surface-elevated);
border-radius: 6px;
font-size: 11px;
transition: all 0.3s ease;
}
[data-theme="light"] .mc-stats {
background: var(--bp-bg);
border: 1px solid var(--bp-border);
}
.mc-stats-item {
display: flex;
align-items: center;
gap: 4px;
}
.mc-modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.mc-modal.hidden {
display: none;
}
.mc-modal-content {
background: var(--bp-bg);
border: 1px solid var(--bp-border-subtle);
border-radius: 16px;
padding: 20px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
[data-theme="light"] .mc-modal-content {
background: var(--bp-surface);
border: 2px solid var(--bp-primary);
box-shadow: 0 8px 32px rgba(108, 27, 27, 0.2);
}
.mc-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.mc-modal-title {
font-size: 18px;
font-weight: 600;
}
.mc-modal-close {
font-size: 11px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--bp-border-subtle);
background: var(--bp-surface-elevated);
color: var(--bp-text);
cursor: pointer;
}
[data-theme="light"] .mc-modal-close {
border: 1px solid var(--bp-primary);
background: var(--bp-surface);
color: var(--bp-primary);
}
[data-theme="light"] .mc-modal-close:hover {
background: var(--bp-primary);
color: white;
}
/* Cloze / Lückentext Styles */
.cloze-preview {
margin-top: 8px;
max-height: 250px;
overflow-y: auto;
}
.cloze-item {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border-subtle);
border-radius: 8px;
padding: 10px;
margin-bottom: 8px;
transition: all 0.3s ease;
}
[data-theme="light"] .cloze-item {
background: var(--bp-surface);
border: 1px solid var(--bp-primary);
box-shadow: 0 2px 8px rgba(108, 27, 27, 0.08);
}
.cloze-sentence {
font-size: 13px;
line-height: 1.8;
color: var(--bp-text);
}
.cloze-gap {
display: inline-block;
min-width: 60px;
border-bottom: 2px solid var(--bp-accent);
margin: 0 2px;
padding: 2px 4px;
text-align: center;
background: var(--bp-accent-soft);
border-radius: 4px 4px 0 0;
}
[data-theme="light"] .cloze-gap {
border-bottom-color: var(--bp-primary);
background: var(--bp-primary-soft);
}
.cloze-gap-input {
width: 80px;
padding: 4px 8px;
font-size: 12px;
border: 1px solid var(--bp-border-subtle);
border-radius: 4px;
background: var(--bp-surface-elevated);
color: var(--bp-text);
text-align: center;
}
[data-theme="light"] .cloze-gap-input {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
}
.cloze-gap-input:focus {
outline: none;
border-color: var(--bp-primary);
}
.cloze-gap-input.correct {
border-color: var(--bp-accent);
background: rgba(90, 191, 96, 0.2);
}
.cloze-gap-input.incorrect {
border-color: var(--bp-danger);
background: rgba(108, 27, 27, 0.2);
}
.cloze-translation {
margin-top: 8px;
padding: 8px;
background: var(--bp-accent-soft);
border: 1px solid var(--bp-accent);
border-radius: 6px;
font-size: 11px;
color: var(--bp-text-muted);
}
[data-theme="light"] .cloze-translation {
background: var(--bp-primary-soft);
border: 1px solid var(--bp-primary);
}
.cloze-translation-label {
font-size: 10px;
color: var(--bp-text-muted);
margin-bottom: 4px;
}
.cloze-hint {
font-size: 10px;
color: var(--bp-text-muted);
font-style: italic;
margin-top: 4px;
}
.cloze-stats {
display: flex;
gap: 12px;
padding: 8px;
background: var(--bp-surface-elevated);
border-radius: 6px;
font-size: 11px;
margin-bottom: 8px;
}
[data-theme="light"] .cloze-stats {
background: var(--bp-bg);
border: 1px solid var(--bp-border);
}
.cloze-feedback {
margin-top: 6px;
font-size: 11px;
padding: 4px 8px;
border-radius: 4px;
}
.cloze-feedback.correct {
background: rgba(34,197,94,0.15);
color: var(--bp-accent);
}
.cloze-feedback.incorrect {
background: rgba(239,68,68,0.15);
color: #ef4444;
}
/* Language Selector */
.language-selector {
margin-right: 8px;
}
.lang-select {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border-subtle);
border-radius: 6px;
color: var(--bp-text);
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
[data-theme="light"] .lang-select {
background: var(--bp-surface);
border: 1px solid var(--bp-primary);
color: var(--bp-text);
}
.lang-select:hover {
border-color: var(--bp-primary);
}
.lang-select:focus {
outline: none;
border-color: var(--bp-primary);
}
[data-theme="light"] .lang-select option {
background: var(--bp-surface);
color: var(--bp-text);
}
/* RTL Support for Arabic */
[dir="rtl"] .sidebar {
border-right: none;
border-left: 1px solid rgba(148,163,184,0.2);
}
[dir="rtl"] .main-layout {
direction: rtl;
}
[dir="rtl"] .card-actions {
flex-direction: row-reverse;
}
/* ==========================================
RICH TEXT EDITOR STYLES
========================================== */
.editor-container {
border: 1px solid var(--bp-border);
border-radius: 8px;
background: var(--bp-surface-elevated);
overflow: hidden;
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 8px 12px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface);
}
.editor-toolbar-group {
display: flex;
gap: 2px;
padding-right: 8px;
margin-right: 8px;
border-right: 1px solid var(--bp-border);
}
.editor-toolbar-group:last-child {
border-right: none;
margin-right: 0;
padding-right: 0;
}
.editor-btn {
padding: 6px 10px;
border: none;
background: transparent;
color: var(--bp-text);
cursor: pointer;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
min-width: 32px;
}
.editor-btn:hover {
background: var(--bp-border-subtle);
}
.editor-btn.active {
background: var(--bp-primary);
color: white;
}
.editor-btn-upload {
background: var(--bp-primary);
color: white;
padding: 6px 12px;
}
.editor-btn-upload:hover {
filter: brightness(1.1);
}
.editor-content {
min-height: 300px;
max-height: 500px;
overflow-y: auto;
padding: 16px;
color: var(--bp-text);
line-height: 1.6;
}
.editor-content:focus {
outline: none;
}
.editor-content[contenteditable="true"] {
cursor: text;
}
.editor-content h1 {
font-size: 24px;
font-weight: 700;
margin: 0 0 16px 0;
}
.editor-content h2 {
font-size: 20px;
font-weight: 600;
margin: 24px 0 12px 0;
}
.editor-content h3 {
font-size: 17px;
font-weight: 600;
margin: 20px 0 10px 0;
}
.editor-content p {
margin: 0 0 12px 0;
}
.editor-content ul, .editor-content ol {
margin: 0 0 12px 0;
padding-left: 24px;
}
.editor-content li {
margin-bottom: 4px;
}
.editor-content strong {
font-weight: 600;
}
.editor-content em {
font-style: italic;
}
.editor-content a {
color: var(--bp-primary);
text-decoration: underline;
}
.editor-content blockquote {
border-left: 3px solid var(--bp-primary);
margin: 16px 0;
padding: 8px 16px;
background: var(--bp-surface);
font-style: italic;
}
.editor-content hr {
border: none;
border-top: 1px solid var(--bp-border);
margin: 20px 0;
}
[data-theme="light"] .editor-container {
border-color: #E0E0E0;
}
[data-theme="light"] .editor-toolbar {
background: #F5F5F5;
border-color: #E0E0E0;
}
[data-theme="light"] .editor-content {
background: #FFFFFF;
}
.word-upload-input {
display: none;
}
.editor-status {
padding: 8px 12px;
font-size: 12px;
color: var(--bp-text-muted);
border-top: 1px solid var(--bp-border);
background: var(--bp-surface);
}
</style>
</head>
<body>
<div class="app-root">
<header class="topbar">
<div class="brand">
<div class="brand-logo">BP</div>
<div>
<div class="brand-text-main">BreakPilot</div>
<div class="brand-text-sub">Studio</div>
</div>
</div>
<nav class="top-nav" style="display:none;">
<div class="top-nav-item active" data-screen="1"></div>
<div class="top-nav-item" data-screen="2"></div>
</nav>
<div class="top-actions">
<div class="language-selector">
<select id="language-select" class="lang-select">
<option value="de">🇩🇪 Deutsch</option>
<option value="tr">🇹🇷 Türkçe</option>
<option value="ar">🇸🇦 العربية</option>
<option value="ru">🇷🇺 Русский</option>
<option value="uk">🇺🇦 Українська</option>
<option value="pl">🇵🇱 Polski</option>
<option value="en">🇬🇧 English</option>
</select>
</div>
<button class="theme-toggle" id="theme-toggle" title="Dark/Light Mode">
<span class="theme-toggle-icon" id="theme-icon">🌙</span>
<span id="theme-label">Dark</span>
</button>
<button class="btn btn-sm btn-ghost" id="btn-legal" title="Rechtliches">⚖️</button>
<button class="btn btn-sm btn-ghost" id="btn-admin" title="Admin Panel">⚙️</button>
<button class="btn btn-sm btn-ghost" id="btn-login" data-i18n="login">Login / Anmeldung</button>
<!-- Notification Bell (shown when logged in) -->
<div class="notification-bell" id="notification-bell">
<button class="notification-bell-btn" id="notification-bell-btn" title="Benachrichtigungen">
🔔
</button>
<span class="notification-badge hidden" id="notification-badge">0</span>
<div class="notification-panel" id="notification-panel">
<div class="notification-panel-header">
<span class="notification-panel-title">Benachrichtigungen</span>
<div class="notification-panel-actions">
<button class="notification-panel-action" onclick="markAllNotificationsRead()" title="Alle als gelesen markieren">✓ Alle</button>
<button class="notification-panel-action" onclick="showNotificationPreferences()" title="Einstellungen">⚙️</button>
</div>
</div>
<div class="notification-list" id="notification-list">
<div class="notification-empty">
<div class="notification-empty-icon">🔔</div>
<div>Keine Benachrichtigungen</div>
</div>
</div>
<div class="notification-footer">
<button class="notification-footer-btn" onclick="loadMoreNotifications()">Mehr laden</button>
</div>
</div>
</div>
<!-- User Dropdown (shown when logged in) -->
<div class="auth-user-dropdown">
<button class="auth-user-btn" id="auth-user-btn">
<div class="auth-user-avatar">BP</div>
<span>▼</span>
</button>
<div class="auth-user-menu" id="auth-user-menu">
<div class="auth-user-menu-header">
<div class="auth-user-menu-name">Benutzer</div>
<div class="auth-user-menu-email">user@example.com</div>
</div>
<button class="auth-user-menu-item" onclick="showProfileModal()">👤 Profil</button>
<button class="auth-user-menu-item" onclick="showSessionsModal()">🔐 Aktive Sitzungen</button>
<button class="auth-user-menu-item danger" onclick="logout()">🚪 Abmelden</button>
</div>
</div>
<div class="pill" data-i18n="mvp_local">MVP · Lokal auf deinem Mac</div>
</div>
</header>
<div class="main-layout">
<aside class="sidebar">
<div>
<div class="sidebar-section-title">Bereiche</div>
<div class="sidebar-menu">
<div class="sidebar-item active">
<div class="sidebar-item-label">
<span>Arbeitsblatt Studio</span>
</div>
<div class="sidebar-item-badge">aktiv</div>
</div>
<div class="sidebar-item">
<div class="sidebar-item-label">
<span>Eltern-Kanal</span>
</div>
<div class="sidebar-item-badge">demnächst</div>
</div>
<div class="sidebar-item">
<div class="sidebar-item-label">
<span>Korrektur / Noten</span>
</div>
<div class="sidebar-item-badge">demnächst</div>
</div>
</div>
</div>
<div>
<div class="sidebar-section-title">Lerneinheiten (lokal)</div>
<div class="unit-form">
<input class="unit-input" id="unit-student" placeholder="Schüler/in">
<input class="unit-input" id="unit-subject" placeholder="Fach">
<input class="unit-input" id="unit-grade" placeholder="Klasse (z.B. 7a)">
<input class="unit-input" id="unit-title" placeholder="Lerneinheit / Thema">
<button type="button" class="btn btn-sm btn-primary btn-unit-add" id="btn-add-unit">Anlegen</button>
<ul class="unit-list" id="unit-list"></ul>
<button type="button" class="btn btn-sm btn-ghost" id="btn-attach-current-to-lu">Aktuelles Arbeitsblatt hinzufügen</button>
</div>
</div>
<div>
<div class="sidebar-section-title">Dateien hochladen</div>
<div class="upload-inline">
<input type="file" id="file-input" multiple>
<button type="button" class="btn btn-sm btn-primary" id="btn-upload-inline">Hochladen</button>
</div>
</div>
<div>
<div class="sidebar-section-title">GPU Infrastruktur</div>
<div id="vast-control" class="vast-control">
<div class="vast-status-row">
<span class="vast-label">Status:</span>
<span id="vast-status-badge" class="vast-badge vast-badge-unknown">Laden...</span>
</div>
<div class="vast-status-row">
<span class="vast-label">GPU:</span>
<span id="vast-gpu-name" class="vast-value">-</span>
</div>
<div class="vast-status-row">
<span class="vast-label">Kosten/h:</span>
<span id="vast-cost-hour" class="vast-value">-</span>
</div>
<div class="vast-status-row">
<span class="vast-label">Auto-Stop:</span>
<span id="vast-auto-shutdown" class="vast-value">-</span>
</div>
<div class="vast-status-row" style="margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--bp-border);">
<span class="vast-label">Budget:</span>
<span id="vast-credit" class="vast-value" style="color: var(--bp-success);">-</span>
</div>
<div class="vast-status-row">
<span class="vast-label">Session:</span>
<span id="vast-session-cost" class="vast-value">-</span>
</div>
<div class="vast-buttons">
<button type="button" class="btn btn-sm btn-primary" id="btn-vast-start" disabled>Starten</button>
<button type="button" class="btn btn-sm btn-danger" id="btn-vast-stop" disabled>Stoppen</button>
</div>
<div id="vast-message" class="vast-message"></div>
</div>
</div>
<div>
<div class="sidebar-section-title">Speicherort</div>
<div class="sidebar-footer">
CI / Brandbook: Farben kommen bereits aus Variablen und sind später leicht anpassbar.
</div>
</div>
</aside>
<main class="content">
<!-- Screen 1: Eingangsdateien & Alt/Neu-Vergleich -->
<section class="panel" id="panel-compare" style="display:flex;">
<div class="panel-header">
<div>
<div class="panel-title">Arbeitsblätter & Vergleich</div>
<div class="panel-subtitle">Links Scan · Rechts neu aufgebautes Arbeitsblatt</div>
<div class="panel-subtitle" id="unit-heading-screen1">Keine Lerneinheit ausgewählt</div>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<button type="button" class="btn btn-sm btn-primary" id="btn-toggle-filter" style="font-size: 10px;">
Nur Lerneinheit
</button>
<span class="small-pill" id="eingang-count">0 Dateien</span>
</div>
</div>
<div class="panel-body">
<ul class="file-list" id="eingang-list"></ul>
<div class="inline-process">
<button type="button" class="btn btn-sm btn-primary" id="btn-full-process">
Arbeitsblätter neu aufbauen
</button>
</div>
<div class="preview-container" id="preview-container">
<div class="preview-placeholder">
Lade Arbeitsblätter hoch und klicke auf „Arbeitsblätter neu aufbauen".<br>
Dann kannst du mit den Pfeilen durch die Scans und die bereinigten Versionen blättern.<br>
Mit Doppelklick öffnest du das Dokument im Vollbild.
</div>
</div>
</div>
</section>
<!-- Screen 2: Kacheln -->
<section class="panel" id="panel-tiles" style="display:none;">
<div class="panel-tools-header">
<div>
<div class="panel-title"><span id="unit-heading-screen2">Keine Lerneinheit ausgewählt</span> Bitte wähle Deine Lernkacheln aus, die Du verwenden möchtest.</div>
</div>
</div>
<div class="panel-body">
<div class="card-toggle-bar">
<button class="toggle-pill active" data-tile="mindmap">Mindmap</button>
<button class="toggle-pill active" data-tile="qa">FrageAntwort</button>
<button class="toggle-pill active" data-tile="mc">Multiple Choice</button>
<button class="toggle-pill active" data-tile="cloze">Lückentext</button>
</div>
<div class="cards-grid">
<!-- Mindmap Lernposter -->
<div class="card card-half" data-tile="mindmap">
<div class="card-header">
<div class="card-title">Mindmap Lernposter</div>
<div class="card-badge" id="mindmap-badge">Bereit</div>
</div>
<div class="card-body">
<div>Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien. Perfekt zum Ausdrucken als A3-Poster!</div>
<div id="mindmap-preview" class="mindmap-preview"></div>
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
<button class="btn btn-sm btn-primary" id="btn-mindmap-generate">Mindmap erstellen</button>
<button class="btn btn-sm btn-ghost" id="btn-mindmap-show" style="display:none;">Ansehen</button>
<button class="btn btn-sm btn-ghost" id="btn-mindmap-print" style="display:none;">A3 Drucken</button>
</div>
</div>
</div>
<!-- FrageAntwort mit Leitner-System -->
<div class="card card-half" data-tile="qa">
<div class="card-header">
<div class="card-title">FrageAntwort-Blatt</div>
<div class="card-badge" id="qa-badge">Bereit</div>
</div>
<div class="card-body">
<div>Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad für nachhaltiges Lernen.</div>
<div id="qa-preview" class="mc-preview"></div>
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
<button class="btn btn-sm btn-primary" id="btn-qa-generate">Q&A generieren</button>
<button class="btn btn-sm btn-ghost" id="btn-qa-learn" style="display:none;">Lernen starten</button>
<button class="btn btn-sm btn-ghost" id="btn-qa-print" style="display:none;">Drucken</button>
</div>
</div>
</div>
<!-- Multiple Choice -->
<div class="card card-half" data-tile="mc">
<div class="card-header">
<div class="card-title">Multiple Choice Test</div>
<div class="card-badge" id="mc-badge">Bereit</div>
</div>
<div class="card-body">
<div>Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.</div>
<div id="mc-preview" class="mc-preview"></div>
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
<button class="btn btn-sm btn-primary" id="btn-mc-generate">MC generieren</button>
<button class="btn btn-sm btn-ghost" id="btn-mc-show" style="display:none;">Quiz starten</button>
<button class="btn btn-sm btn-ghost" id="btn-mc-print" style="display:none;">Drucken</button>
</div>
</div>
</div>
<!-- Lückentext -->
<div class="card card-half" data-tile="cloze">
<div class="card-header">
<div class="card-title">Lückentext</div>
<div class="card-badge" id="cloze-badge">Bereit</div>
</div>
<div class="card-body">
<div>Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.</div>
<div class="cloze-language-select" style="margin-top:8px;">
<label style="font-size:11px;color:var(--bp-text-muted);">Übersetzung:</label>
<select id="cloze-language" class="unit-input" style="padding:4px 8px;font-size:11px;width:auto;">
<option value="tr">Türkisch</option>
<option value="ar">Arabisch</option>
<option value="ru">Russisch</option>
<option value="uk">Ukrainisch</option>
<option value="pl">Polnisch</option>
<option value="en">Englisch</option>
<option value="fr">Französisch</option>
<option value="es">Spanisch</option>
</select>
</div>
<div id="cloze-preview" class="cloze-preview"></div>
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
<button class="btn btn-sm btn-primary" id="btn-cloze-generate">Lückentext generieren</button>
<button class="btn btn-sm btn-ghost" id="btn-cloze-show" style="display:none;">Übung starten</button>
<button class="btn btn-sm btn-ghost" id="btn-cloze-print" style="display:none;">Drucken</button>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<div class="pager">
<button id="pager-prev">&lt;</button>
<span id="pager-label">1 von 2</span>
<button id="pager-next">&gt;</button>
</div>
<div class="status-bar" id="status-bar">
<div class="status-dot" id="status-dot"></div>
<div>
<div class="status-text-main" id="status-main"></div>
<div class="status-text-sub" id="status-sub"></div>
</div>
</div>
</div>
<!-- Vollbild-Overlay -->
<div class="lightbox hidden" id="lightbox">
<div class="lightbox-inner">
<button type="button" class="lightbox-close" id="lightbox-close">Schließen ✕</button>
<img id="lightbox-img" class="lightbox-img" src="" alt="">
<div id="lightbox-caption" class="lightbox-caption"></div>
</div>
</div>
<!-- MC Quiz Modal -->
<div class="mc-modal hidden" id="mc-modal">
<div class="mc-modal-content">
<div class="mc-modal-header">
<div class="mc-modal-title">Multiple Choice Quiz</div>
<button type="button" class="mc-modal-close" id="mc-modal-close">Schließen ✕</button>
</div>
<div id="mc-modal-body"></div>
</div>
</div>
<!-- Cloze / Lückentext Modal -->
<div class="mc-modal hidden" id="cloze-modal">
<div class="mc-modal-content" style="max-width:700px;">
<div class="mc-modal-header">
<div class="mc-modal-title">Lückentext-Übung</div>
<button type="button" class="mc-modal-close" id="cloze-modal-close">Schließen ✕</button>
</div>
<div id="cloze-modal-body"></div>
</div>
</div>
<!-- Q&A / Frage-Antwort Modal mit Leitner-System -->
<div class="mc-modal hidden" id="qa-modal">
<div class="mc-modal-content" style="max-width:700px;">
<div class="mc-modal-header">
<div class="mc-modal-title">Frage-Antwort Lernmodus</div>
<button type="button" class="mc-modal-close" id="qa-modal-close">Schließen ✕</button>
</div>
<div id="qa-modal-body"></div>
</div>
</div>
<div class="footer">
<span>BreakPilot · MVP lokal</span>
<span>·</span>
<a href="#" onclick="openImprintModal(); return false;">Impressum</a>
<a href="#" onclick="openLegalModal(); return false;">Rechtliches</a>
</div>
<script>
// ==========================================
// THEME TOGGLE (Dark/Light Mode)
// ==========================================
(function() {
const savedTheme = localStorage.getItem('bp-theme') || 'dark';
if (savedTheme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
}
})();
function initThemeToggle() {
const toggle = document.getElementById('theme-toggle');
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
function updateToggleUI(theme) {
if (theme === 'light') {
icon.textContent = '☀️';
label.textContent = 'Light';
} else {
icon.textContent = '🌙';
label.textContent = 'Dark';
}
}
// Initialize UI based on current theme
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
updateToggleUI(currentTheme);
toggle.addEventListener('click', function() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const newTheme = current === 'dark' ? 'light' : 'dark';
if (newTheme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
} else {
document.documentElement.removeAttribute('data-theme');
}
localStorage.setItem('bp-theme', newTheme);
updateToggleUI(newTheme);
});
}
// ==========================================
// INTERNATIONALISIERUNG (i18n)
// ==========================================
const translations = {
de: {
// Navigation & Header
brand_sub: "Studio",
nav_compare: "Arbeitsblätter",
nav_tiles: "Lern-Kacheln",
login: "Login / Anmeldung",
mvp_local: "MVP · Lokal auf deinem Mac",
// Sidebar
sidebar_areas: "Bereiche",
sidebar_studio: "Arbeitsblatt Studio",
sidebar_active: "aktiv",
sidebar_parents: "Eltern-Kanal",
sidebar_soon: "demnächst",
sidebar_correction: "Korrektur / Noten",
sidebar_units: "Lerneinheiten (lokal)",
input_student: "Schüler/in",
input_subject: "Fach",
input_grade: "Klasse (z.B. 7a)",
input_unit_title: "Lerneinheit / Thema",
btn_create: "Anlegen",
btn_add_current: "Aktuelles Arbeitsblatt hinzufügen",
btn_filter_unit: "Nur Lerneinheit",
btn_filter_all: "Alle Dateien",
// Screen 1 - Compare
uploaded_worksheets: "Hochgeladene Arbeitsblätter",
files: "Dateien",
btn_upload: "Hochladen",
btn_delete: "Löschen",
original_scan: "Original-Scan",
cleaned_version: "Bereinigt (Handschrift entfernt)",
no_cleaned: "Noch keine bereinigte Version vorhanden.",
process_hint: "Klicke auf 'Verarbeiten', um das Arbeitsblatt zu analysieren und zu bereinigen.",
worksheet_print: "Drucken",
worksheet_no_data: "Keine Arbeitsblatt-Daten vorhanden.",
btn_full_process: "Verarbeiten (Analyse + Bereinigung + HTML)",
btn_original_generate: "Nur Original-HTML generieren",
// Screen 2 - Tiles
learning_unit: "Lerneinheit",
no_unit_selected: "Keine Lerneinheit ausgewählt",
// MC Tile
mc_title: "Multiple Choice Test",
mc_ready: "Bereit",
mc_generating: "Generiert...",
mc_done: "Fertig",
mc_error: "Fehler",
mc_desc: "Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.",
mc_generate: "MC generieren",
mc_show: "Fragen anzeigen",
mc_quiz_title: "Multiple Choice Quiz",
mc_evaluate: "Auswerten",
mc_correct: "Richtig!",
mc_incorrect: "Leider falsch.",
mc_not_answered: "Nicht beantwortet. Richtig wäre:",
mc_result: "von",
mc_result_correct: "richtig",
mc_percent: "korrekt",
mc_no_questions: "Noch keine MC-Fragen für dieses Arbeitsblatt generiert.",
mc_print: "Drucken",
mc_print_with_answers: "Mit Lösungen drucken?",
// Cloze Tile
cloze_title: "Lückentext",
cloze_desc: "Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.",
cloze_translation: "Übersetzung:",
cloze_generate: "Lückentext generieren",
cloze_start: "Übung starten",
cloze_exercise_title: "Lückentext-Übung",
cloze_instruction: "Fülle die Lücken aus und klicke auf 'Prüfen'.",
cloze_check: "Prüfen",
cloze_show_answers: "Lösungen zeigen",
cloze_no_texts: "Noch keine Lückentexte für dieses Arbeitsblatt generiert.",
cloze_sentences: "Sätze",
cloze_gaps: "Lücken",
cloze_gaps_total: "Lücken gesamt",
cloze_with_gaps: "(mit Lücken)",
cloze_print: "Drucken",
cloze_print_with_answers: "Mit Lösungen drucken?",
// QA Tile
qa_title: "Frage-Antwort-Blatt",
qa_desc: "Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad.",
qa_generate: "Q&A generieren",
qa_learn: "Lernen starten",
qa_print: "Drucken",
qa_no_questions: "Noch keine Q&A für dieses Arbeitsblatt generiert.",
qa_box_new: "Neu",
qa_box_learning: "Gelernt",
qa_box_mastered: "Gefestigt",
qa_show_answer: "Antwort zeigen",
qa_your_answer: "Deine Antwort",
qa_type_answer: "Schreibe deine Antwort hier...",
qa_check_answer: "Antwort prüfen",
qa_correct_answer: "Richtige Antwort",
qa_self_evaluate: "War deine Antwort richtig?",
qa_no_answer: "(keine Antwort eingegeben)",
qa_correct: "Richtig",
qa_incorrect: "Falsch",
qa_key_terms: "Schlüsselbegriffe",
qa_session_correct: "Richtig",
qa_session_incorrect: "Falsch",
qa_session_complete: "Lernrunde abgeschlossen!",
qa_result_correct: "richtig",
qa_restart: "Nochmal lernen",
qa_print_with_answers: "Mit Lösungen drucken?",
question: "Frage",
answer: "Antwort",
status_generating_qa: "Generiere Q&A …",
status_qa_generated: "Q&A generiert",
// Common
close: "Schließen",
subject: "Fach",
grade: "Stufe",
questions: "Fragen",
worksheet: "Arbeitsblatt",
loading: "Lädt...",
error: "Fehler",
success: "Erfolgreich",
// Footer
imprint: "Impressum",
privacy: "Datenschutz",
contact: "Kontakt",
// Status messages
status_ready: "Bereit",
status_processing: "Verarbeitet...",
status_generating_mc: "Generiere MC-Fragen …",
status_generating_cloze: "Generiere Lückentexte …",
status_please_wait: "Bitte warten, KI arbeitet.",
status_mc_generated: "MC-Fragen generiert",
status_cloze_generated: "Lückentexte generiert",
status_files_created: "Dateien erstellt",
// Mindmap Tile
mindmap_title: "Mindmap Lernposter",
mindmap_desc: "Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien.",
mindmap_generate: "Mindmap erstellen",
mindmap_show: "Ansehen",
mindmap_print_a3: "A3 Drucken",
generating_mindmap: "Erstelle Mindmap...",
mindmap_generated: "Mindmap erstellt!",
no_analysis: "Keine Analyse",
analyze_first: "Bitte zuerst analysieren (Verarbeiten starten)",
categories: "Kategorien",
terms: "Begriffe",
},
tr: {
brand_sub: "Stüdyo",
nav_compare: "Çalışma Sayfaları",
nav_tiles: "Öğrenme Kartları",
login: "Giriş / Kayıt",
mvp_local: "MVP · Mac'inizde Yerel",
sidebar_areas: "Alanlar",
sidebar_studio: "Çalışma Sayfası Stüdyosu",
sidebar_active: "aktif",
sidebar_parents: "Ebeveyn Kanalı",
sidebar_soon: "yakında",
sidebar_correction: "Düzeltme / Notlar",
sidebar_units: "Öğrenme Birimleri (yerel)",
input_student: "Öğrenci",
input_subject: "Ders",
input_grade: "Sınıf (örn. 7a)",
input_unit_title: "Öğrenme Birimi / Konu",
btn_create: "Oluştur",
btn_add_current: "Mevcut çalışma sayfasını ekle",
btn_filter_unit: "Sadece Birim",
btn_filter_all: "Tüm Dosyalar",
uploaded_worksheets: "Yüklenen Çalışma Sayfaları",
files: "Dosya",
btn_upload: "Yükle",
btn_delete: "Sil",
original_scan: "Orijinal Tarama",
cleaned_version: "Temizlenmiş (El yazısı kaldırıldı)",
no_cleaned: "Henüz temizlenmiş sürüm yok.",
process_hint: "Çalışma sayfasını analiz etmek ve temizlemek için 'İşle'ye tıklayın.",
worksheet_print: "Yazdır",
worksheet_no_data: "Çalışma sayfası verisi yok.",
btn_full_process: "İşle (Analiz + Temizleme + HTML)",
btn_original_generate: "Sadece Orijinal HTML Oluştur",
learning_unit: "Öğrenme Birimi",
no_unit_selected: "Öğrenme birimi seçilmedi",
mc_title: "Çoktan Seçmeli Test",
mc_ready: "Hazır",
mc_generating: "Oluşturuluyor...",
mc_done: "Tamamlandı",
mc_error: "Hata",
mc_desc: "Orijinal zorluğa uygun (örn. 7. sınıf) çoktan seçmeli sorular oluşturur.",
mc_generate: "ÇS Oluştur",
mc_show: "Soruları Göster",
mc_quiz_title: "Çoktan Seçmeli Quiz",
mc_evaluate: "Değerlendir",
mc_correct: "Doğru!",
mc_incorrect: "Maalesef yanlış.",
mc_not_answered: "Cevaplanmadı. Doğru cevap:",
mc_result: "/",
mc_result_correct: "doğru",
mc_percent: "doğru",
mc_no_questions: "Bu çalışma sayfası için henüz ÇS sorusu oluşturulmadı.",
mc_print: "Yazdır",
mc_print_with_answers: "Cevaplarla yazdır?",
cloze_title: "Boşluk Doldurma",
cloze_desc: "Her cümlede birden fazla anlamlı boşluk içeren metinler oluşturur. Ebeveynler için çeviri dahil.",
cloze_translation: "Çeviri:",
cloze_generate: "Boşluk Metni Oluştur",
cloze_start: "Alıştırmayı Başlat",
cloze_exercise_title: "Boşluk Doldurma Alıştırması",
cloze_instruction: "Boşlukları doldurun ve 'Kontrol Et'e tıklayın.",
cloze_check: "Kontrol Et",
cloze_show_answers: "Cevapları Göster",
cloze_no_texts: "Bu çalışma sayfası için henüz boşluk metni oluşturulmadı.",
cloze_sentences: "Cümle",
cloze_gaps: "Boşluk",
cloze_gaps_total: "Toplam boşluk",
cloze_with_gaps: "(boşluklu)",
cloze_print: "Yazdır",
cloze_print_with_answers: "Cevaplarla yazdır?",
qa_title: "Soru-Cevap Sayfası",
qa_desc: "Leitner kutu sistemiyle soru-cevap çiftleri. Zorluk derecesine göre tekrar.",
qa_generate: "S&C Oluştur",
qa_learn: "Öğrenmeye Başla",
qa_print: "Yazdır",
qa_no_questions: "Bu çalışma sayfası için henüz S&C oluşturulmadı.",
qa_box_new: "Yeni",
qa_box_learning: "Öğreniliyor",
qa_box_mastered: "Pekiştirildi",
qa_show_answer: "Cevabı Göster",
qa_your_answer: "Senin Cevabın",
qa_type_answer: "Cevabını buraya yaz...",
qa_check_answer: "Cevabı Kontrol Et",
qa_correct_answer: "Doğru Cevap",
qa_self_evaluate: "Cevabın doğru muydu?",
qa_no_answer: "(cevap girilmedi)",
qa_correct: "Doğru",
qa_incorrect: "Yanlış",
qa_key_terms: "Anahtar Kavramlar",
qa_session_correct: "Doğru",
qa_session_incorrect: "Yanlış",
qa_session_complete: "Öğrenme turu tamamlandı!",
qa_result_correct: "doğru",
qa_restart: "Tekrar Öğren",
qa_print_with_answers: "Cevaplarla yazdır?",
question: "Soru",
answer: "Cevap",
status_generating_qa: "S&C oluşturuluyor…",
status_qa_generated: "S&C oluşturuldu",
close: "Kapat",
subject: "Ders",
grade: "Seviye",
questions: "Soru",
worksheet: "Çalışma Sayfası",
loading: "Yükleniyor...",
error: "Hata",
success: "Başarılı",
imprint: "Künye",
privacy: "Gizlilik",
contact: "İletişim",
status_ready: "Hazır",
status_processing: "İşleniyor...",
status_generating_mc: "ÇS soruları oluşturuluyor…",
status_generating_cloze: "Boşluk metinleri oluşturuluyor…",
status_please_wait: "Lütfen bekleyin, yapay zeka çalışıyor.",
status_mc_generated: "ÇS soruları oluşturuldu",
status_cloze_generated: "Boşluk metinleri oluşturuldu",
status_files_created: "dosya oluşturuldu",
// Mindmap Tile
mindmap_title: "Zihin Haritası Poster",
mindmap_desc: "Ana konuyu ortada ve tüm terimleri renkli kategorilerde gösteren çocuk dostu bir zihin haritası oluşturur.",
mindmap_generate: "Zihin Haritası Oluştur",
mindmap_show: "Görüntüle",
mindmap_print_a3: "A3 Yazdır",
generating_mindmap: "Zihin haritası oluşturuluyor...",
mindmap_generated: "Zihin haritası oluşturuldu!",
no_analysis: "Analiz yok",
analyze_first: "Lütfen önce analiz edin (İşle'ye tıklayın)",
categories: "Kategoriler",
terms: "Terimler",
},
ar: {
brand_sub: "ستوديو",
nav_compare: "أوراق العمل",
nav_tiles: "بطاقات التعلم",
login: "تسجيل الدخول / التسجيل",
mvp_local: "MVP · محلي على جهازك",
sidebar_areas: "الأقسام",
sidebar_studio: "استوديو أوراق العمل",
sidebar_active: "نشط",
sidebar_parents: "قناة الوالدين",
sidebar_soon: "قريباً",
sidebar_correction: "التصحيح / الدرجات",
sidebar_units: "وحدات التعلم (محلية)",
input_student: "الطالب/ة",
input_subject: "المادة",
input_grade: "الصف (مثل 7أ)",
input_unit_title: "وحدة التعلم / الموضوع",
btn_create: "إنشاء",
btn_add_current: "إضافة ورقة العمل الحالية",
btn_filter_unit: "الوحدة فقط",
btn_filter_all: "جميع الملفات",
uploaded_worksheets: "أوراق العمل المحملة",
files: "ملفات",
btn_upload: "تحميل",
btn_delete: "حذف",
original_scan: "المسح الأصلي",
cleaned_version: "منظف (تم إزالة الكتابة اليدوية)",
no_cleaned: "لا توجد نسخة منظفة بعد.",
process_hint: "انقر على 'معالجة' لتحليل وتنظيف ورقة العمل.",
worksheet_print: "طباعة",
worksheet_no_data: "لا توجد بيانات ورقة العمل.",
btn_full_process: "معالجة (تحليل + تنظيف + HTML)",
btn_original_generate: "إنشاء HTML الأصلي فقط",
learning_unit: "وحدة التعلم",
no_unit_selected: "لم يتم اختيار وحدة تعلم",
mc_title: "اختبار متعدد الخيارات",
mc_ready: "جاهز",
mc_generating: "جاري الإنشاء...",
mc_done: "تم",
mc_error: "خطأ",
mc_desc: "ينشئ أسئلة اختيار من متعدد تناسب مستوى الصعوبة الأصلي (مثل الصف 7).",
mc_generate: "إنشاء أسئلة",
mc_show: "عرض الأسئلة",
mc_quiz_title: "اختبار متعدد الخيارات",
mc_evaluate: "تقييم",
mc_correct: "صحيح!",
mc_incorrect: "للأسف خطأ.",
mc_not_answered: "لم تتم الإجابة. الإجابة الصحيحة:",
mc_result: "من",
mc_result_correct: "صحيح",
mc_percent: "صحيح",
mc_no_questions: "لم يتم إنشاء أسئلة بعد لورقة العمل هذه.",
mc_print: "طباعة",
mc_print_with_answers: "طباعة مع الإجابات؟",
cloze_title: "ملء الفراغات",
cloze_desc: "ينشئ نصوصاً بفراغات متعددة في كل جملة. يشمل الترجمة للوالدين.",
cloze_translation: "الترجمة:",
cloze_generate: "إنشاء نص الفراغات",
cloze_start: "بدء التمرين",
cloze_exercise_title: "تمرين ملء الفراغات",
cloze_instruction: "املأ الفراغات وانقر على 'تحقق'.",
cloze_check: "تحقق",
cloze_show_answers: "عرض الإجابات",
cloze_no_texts: "لم يتم إنشاء نصوص فراغات بعد لورقة العمل هذه.",
cloze_sentences: "جمل",
cloze_gaps: "فراغات",
cloze_gaps_total: "إجمالي الفراغات",
cloze_with_gaps: "(مع فراغات)",
cloze_print: "طباعة",
cloze_print_with_answers: "طباعة مع الإجابات؟",
qa_title: "ورقة الأسئلة والأجوبة",
qa_desc: "أزواج أسئلة وأجوبة مع نظام صندوق لايتنر. التكرار حسب الصعوبة.",
qa_generate: "إنشاء س&ج",
qa_learn: "بدء التعلم",
qa_print: "طباعة",
qa_no_questions: "لم يتم إنشاء س&ج بعد لورقة العمل هذه.",
qa_box_new: "جديد",
qa_box_learning: "قيد التعلم",
qa_box_mastered: "متقن",
qa_show_answer: "عرض الإجابة",
qa_your_answer: "إجابتك",
qa_type_answer: "اكتب إجابتك هنا...",
qa_check_answer: "تحقق من الإجابة",
qa_correct_answer: "الإجابة الصحيحة",
qa_self_evaluate: "هل كانت إجابتك صحيحة؟",
qa_no_answer: "(لم يتم إدخال إجابة)",
qa_correct: "صحيح",
qa_incorrect: "خطأ",
qa_key_terms: "المصطلحات الرئيسية",
qa_session_correct: "صحيح",
qa_session_incorrect: "خطأ",
qa_session_complete: "اكتملت جولة التعلم!",
qa_result_correct: "صحيح",
qa_restart: "تعلم مرة أخرى",
qa_print_with_answers: "طباعة مع الإجابات؟",
question: "سؤال",
answer: "إجابة",
status_generating_qa: "جاري إنشاء س&ج…",
status_qa_generated: "تم إنشاء س&ج",
close: "إغلاق",
subject: "المادة",
grade: "المستوى",
questions: "أسئلة",
worksheet: "ورقة العمل",
loading: "جاري التحميل...",
error: "خطأ",
success: "نجاح",
imprint: "البصمة",
privacy: "الخصوصية",
contact: "اتصل بنا",
status_ready: "جاهز",
status_processing: "جاري المعالجة...",
status_generating_mc: "جاري إنشاء الأسئلة…",
status_generating_cloze: "جاري إنشاء نصوص الفراغات…",
status_please_wait: "يرجى الانتظار، الذكاء الاصطناعي يعمل.",
status_mc_generated: "تم إنشاء الأسئلة",
status_cloze_generated: "تم إنشاء نصوص الفراغات",
status_files_created: "ملفات تم إنشاؤها",
// Mindmap Tile
mindmap_title: "ملصق خريطة ذهنية",
mindmap_desc: "ينشئ خريطة ذهنية مناسبة للأطفال مع الموضوع الرئيسي في المنتصف وجميع المصطلحات في فئات ملونة.",
mindmap_generate: "إنشاء خريطة ذهنية",
mindmap_show: "عرض",
mindmap_print_a3: "طباعة A3",
generating_mindmap: "جاري إنشاء الخريطة الذهنية...",
mindmap_generated: "تم إنشاء الخريطة الذهنية!",
no_analysis: "لا يوجد تحليل",
analyze_first: "يرجى التحليل أولاً (انقر على معالجة)",
categories: "الفئات",
terms: "المصطلحات",
},
ru: {
brand_sub: "Студия",
nav_compare: "Рабочие листы",
nav_tiles: "Учебные карточки",
login: "Вход / Регистрация",
mvp_local: "MVP · Локально на вашем Mac",
sidebar_areas: "Разделы",
sidebar_studio: "Студия рабочих листов",
sidebar_active: "активно",
sidebar_parents: "Канал для родителей",
sidebar_soon: "скоро",
sidebar_correction: "Проверка / Оценки",
sidebar_units: "Учебные блоки (локально)",
input_student: "Ученик",
input_subject: "Предмет",
input_grade: "Класс (напр. 7а)",
input_unit_title: "Учебный блок / Тема",
btn_create: "Создать",
btn_add_current: "Добавить текущий лист",
btn_filter_unit: "Только блок",
btn_filter_all: "Все файлы",
uploaded_worksheets: "Загруженные рабочие листы",
files: "файлов",
btn_upload: "Загрузить",
btn_delete: "Удалить",
original_scan: "Оригинальный скан",
cleaned_version: "Очищено (рукопись удалена)",
no_cleaned: "Очищенная версия пока недоступна.",
process_hint: "Нажмите 'Обработать' для анализа и очистки листа.",
worksheet_print: "Печать",
worksheet_no_data: "Нет данных рабочего листа.",
btn_full_process: "Обработать (Анализ + Очистка + HTML)",
btn_original_generate: "Только оригинальный HTML",
learning_unit: "Учебный блок",
no_unit_selected: "Блок не выбран",
mc_title: "Тест с выбором ответа",
mc_ready: "Готово",
mc_generating: "Создается...",
mc_done: "Готово",
mc_error: "Ошибка",
mc_desc: "Создает вопросы с выбором ответа соответствующей сложности (напр. 7 класс).",
mc_generate: "Создать тест",
mc_show: "Показать вопросы",
mc_quiz_title: "Тест с выбором ответа",
mc_evaluate: "Оценить",
mc_correct: "Правильно!",
mc_incorrect: "К сожалению, неверно.",
mc_not_answered: "Нет ответа. Правильный ответ:",
mc_result: "из",
mc_result_correct: "правильно",
mc_percent: "верно",
mc_no_questions: "Вопросы для этого листа еще не созданы.",
mc_print: "Печать",
mc_print_with_answers: "Печатать с ответами?",
cloze_title: "Текст с пропусками",
cloze_desc: "Создает тексты с несколькими пропусками в каждом предложении. Включая перевод для родителей.",
cloze_translation: "Перевод:",
cloze_generate: "Создать текст",
cloze_start: "Начать упражнение",
cloze_exercise_title: "Упражнение с пропусками",
cloze_instruction: "Заполните пропуски и нажмите 'Проверить'.",
cloze_check: "Проверить",
cloze_show_answers: "Показать ответы",
cloze_no_texts: "Тексты для этого листа еще не созданы.",
cloze_sentences: "предложений",
cloze_gaps: "пропусков",
cloze_gaps_total: "Всего пропусков",
cloze_with_gaps: "(с пропусками)",
cloze_print: "Печать",
cloze_print_with_answers: "Печатать с ответами?",
qa_title: "Лист вопросов и ответов",
qa_desc: "Пары вопрос-ответ с системой Лейтнера. Повторение по уровню сложности.",
qa_generate: "Создать В&О",
qa_learn: "Начать обучение",
qa_print: "Печать",
qa_no_questions: "В&О для этого листа еще не созданы.",
qa_box_new: "Новый",
qa_box_learning: "Изучается",
qa_box_mastered: "Освоено",
qa_show_answer: "Показать ответ",
qa_your_answer: "Твой ответ",
qa_type_answer: "Напиши свой ответ здесь...",
qa_check_answer: "Проверить ответ",
qa_correct_answer: "Правильный ответ",
qa_self_evaluate: "Твой ответ был правильным?",
qa_no_answer: "(ответ не введён)",
qa_correct: "Правильно",
qa_incorrect: "Неправильно",
qa_key_terms: "Ключевые термины",
qa_session_correct: "Правильно",
qa_session_incorrect: "Неправильно",
qa_session_complete: "Раунд обучения завершен!",
qa_result_correct: "правильно",
qa_restart: "Учить снова",
qa_print_with_answers: "Печатать с ответами?",
question: "Вопрос",
answer: "Ответ",
status_generating_qa: "Создание В&О…",
status_qa_generated: "В&О созданы",
close: "Закрыть",
subject: "Предмет",
grade: "Уровень",
questions: "вопросов",
worksheet: "Рабочий лист",
loading: "Загрузка...",
error: "Ошибка",
success: "Успешно",
imprint: "Импрессум",
privacy: "Конфиденциальность",
contact: "Контакт",
status_ready: "Готово",
status_processing: "Обработка...",
status_generating_mc: "Создание вопросов…",
status_generating_cloze: "Создание текстов…",
status_please_wait: "Пожалуйста, подождите, ИИ работает.",
status_mc_generated: "Вопросы созданы",
status_cloze_generated: "Тексты созданы",
status_files_created: "файлов создано",
// Mindmap Tile
mindmap_title: "Плакат Майнд-карта",
mindmap_desc: "Создает детскую ментальную карту с главной темой в центре и всеми терминами в цветных категориях.",
mindmap_generate: "Создать карту",
mindmap_show: "Просмотр",
mindmap_print_a3: "Печать A3",
generating_mindmap: "Создание карты...",
mindmap_generated: "Карта создана!",
no_analysis: "Нет анализа",
analyze_first: "Сначала выполните анализ (нажмите Обработать)",
categories: "Категории",
terms: "Термины",
},
uk: {
brand_sub: "Студія",
nav_compare: "Робочі аркуші",
nav_tiles: "Навчальні картки",
login: "Вхід / Реєстрація",
mvp_local: "MVP · Локально на вашому Mac",
sidebar_areas: "Розділи",
sidebar_studio: "Студія робочих аркушів",
sidebar_active: "активно",
sidebar_parents: "Канал для батьків",
sidebar_soon: "незабаром",
sidebar_correction: "Перевірка / Оцінки",
sidebar_units: "Навчальні блоки (локально)",
input_student: "Учень",
input_subject: "Предмет",
input_grade: "Клас (напр. 7а)",
input_unit_title: "Навчальний блок / Тема",
btn_create: "Створити",
btn_add_current: "Додати поточний аркуш",
btn_filter_unit: "Лише блок",
btn_filter_all: "Усі файли",
uploaded_worksheets: "Завантажені робочі аркуші",
files: "файлів",
btn_upload: "Завантажити",
btn_delete: "Видалити",
original_scan: "Оригінальний скан",
cleaned_version: "Очищено (рукопис видалено)",
no_cleaned: "Очищена версія ще недоступна.",
process_hint: "Натисніть 'Обробити' для аналізу та очищення аркуша.",
worksheet_print: "Друк",
worksheet_no_data: "Немає даних робочого аркуша.",
btn_full_process: "Обробити (Аналіз + Очищення + HTML)",
btn_original_generate: "Лише оригінальний HTML",
learning_unit: "Навчальний блок",
no_unit_selected: "Блок не вибрано",
mc_title: "Тест з вибором відповіді",
mc_ready: "Готово",
mc_generating: "Створюється...",
mc_done: "Готово",
mc_error: "Помилка",
mc_desc: "Створює питання з вибором відповіді відповідної складності (напр. 7 клас).",
mc_generate: "Створити тест",
mc_show: "Показати питання",
mc_quiz_title: "Тест з вибором відповіді",
mc_evaluate: "Оцінити",
mc_correct: "Правильно!",
mc_incorrect: "На жаль, неправильно.",
mc_not_answered: "Немає відповіді. Правильна відповідь:",
mc_result: "з",
mc_result_correct: "правильно",
mc_percent: "вірно",
mc_no_questions: "Питання для цього аркуша ще не створені.",
mc_print: "Друк",
mc_print_with_answers: "Друкувати з відповідями?",
cloze_title: "Текст з пропусками",
cloze_desc: "Створює тексти з кількома пропусками в кожному реченні. Включаючи переклад для батьків.",
cloze_translation: "Переклад:",
cloze_generate: "Створити текст",
cloze_start: "Почати вправу",
cloze_exercise_title: "Вправа з пропусками",
cloze_instruction: "Заповніть пропуски та натисніть 'Перевірити'.",
cloze_check: "Перевірити",
cloze_show_answers: "Показати відповіді",
cloze_no_texts: "Тексти для цього аркуша ще не створені.",
cloze_sentences: "речень",
cloze_gaps: "пропусків",
cloze_gaps_total: "Всього пропусків",
cloze_with_gaps: "(з пропусками)",
cloze_print: "Друк",
cloze_print_with_answers: "Друкувати з відповідями?",
qa_title: "Аркуш питань і відповідей",
qa_desc: "Пари питання-відповідь з системою Лейтнера. Повторення за рівнем складності.",
qa_generate: "Створити П&В",
qa_learn: "Почати навчання",
qa_print: "Друк",
qa_no_questions: "П&В для цього аркуша ще не створені.",
qa_box_new: "Новий",
qa_box_learning: "Вивчається",
qa_box_mastered: "Засвоєно",
qa_show_answer: "Показати відповідь",
qa_your_answer: "Твоя відповідь",
qa_type_answer: "Напиши свою відповідь тут...",
qa_check_answer: "Перевірити відповідь",
qa_correct_answer: "Правильна відповідь",
qa_self_evaluate: "Твоя відповідь була правильною?",
qa_no_answer: "(відповідь не введена)",
qa_correct: "Правильно",
qa_incorrect: "Неправильно",
qa_key_terms: "Ключові терміни",
qa_session_correct: "Правильно",
qa_session_incorrect: "Неправильно",
qa_session_complete: "Раунд навчання завершено!",
qa_result_correct: "правильно",
qa_restart: "Вчити знову",
qa_print_with_answers: "Друкувати з відповідями?",
question: "Питання",
answer: "Відповідь",
status_generating_qa: "Створення П&В…",
status_qa_generated: "П&В створені",
close: "Закрити",
subject: "Предмет",
grade: "Рівень",
questions: "питань",
worksheet: "Робочий аркуш",
loading: "Завантаження...",
error: "Помилка",
success: "Успішно",
imprint: "Імпресум",
privacy: "Конфіденційність",
contact: "Контакт",
status_ready: "Готово",
status_processing: "Обробка...",
status_generating_mc: "Створення питань…",
status_generating_cloze: "Створення текстів…",
status_please_wait: "Будь ласка, зачекайте, ШІ працює.",
status_mc_generated: "Питання створені",
status_cloze_generated: "Тексти створені",
status_files_created: "файлів створено",
// Mindmap Tile
mindmap_title: "Плакат Інтелект-карта",
mindmap_desc: "Створює дитячу інтелект-карту з головною темою в центрі та всіма термінами в кольорових категоріях.",
mindmap_generate: "Створити карту",
mindmap_show: "Переглянути",
mindmap_print_a3: "Друк A3",
generating_mindmap: "Створення карти...",
mindmap_generated: "Карту створено!",
no_analysis: "Немає аналізу",
analyze_first: "Спочатку виконайте аналіз (натисніть Обробити)",
categories: "Категорії",
terms: "Терміни",
},
pl: {
brand_sub: "Studio",
nav_compare: "Karty pracy",
nav_tiles: "Karty nauki",
login: "Logowanie / Rejestracja",
mvp_local: "MVP · Lokalnie na Twoim Mac",
sidebar_areas: "Sekcje",
sidebar_studio: "Studio kart pracy",
sidebar_active: "aktywne",
sidebar_parents: "Kanał dla rodziców",
sidebar_soon: "wkrótce",
sidebar_correction: "Korekta / Oceny",
sidebar_units: "Jednostki nauki (lokalnie)",
input_student: "Uczeń",
input_subject: "Przedmiot",
input_grade: "Klasa (np. 7a)",
input_unit_title: "Jednostka nauki / Temat",
btn_create: "Utwórz",
btn_add_current: "Dodaj bieżącą kartę",
btn_filter_unit: "Tylko jednostka",
btn_filter_all: "Wszystkie pliki",
uploaded_worksheets: "Przesłane karty pracy",
files: "plików",
btn_upload: "Prześlij",
btn_delete: "Usuń",
original_scan: "Oryginalny skan",
cleaned_version: "Oczyszczone (pismo ręczne usunięte)",
no_cleaned: "Oczyszczona wersja jeszcze niedostępna.",
process_hint: "Kliknij 'Przetwórz', aby przeanalizować i oczyścić kartę.",
worksheet_print: "Drukuj",
worksheet_no_data: "Brak danych arkusza.",
btn_full_process: "Przetwórz (Analiza + Czyszczenie + HTML)",
btn_original_generate: "Tylko oryginalny HTML",
learning_unit: "Jednostka nauki",
no_unit_selected: "Nie wybrano jednostki",
mc_title: "Test wielokrotnego wyboru",
mc_ready: "Gotowe",
mc_generating: "Tworzenie...",
mc_done: "Gotowe",
mc_error: "Błąd",
mc_desc: "Tworzy pytania wielokrotnego wyboru o odpowiednim poziomie trudności (np. klasa 7).",
mc_generate: "Utwórz test",
mc_show: "Pokaż pytania",
mc_quiz_title: "Test wielokrotnego wyboru",
mc_evaluate: "Oceń",
mc_correct: "Dobrze!",
mc_incorrect: "Niestety źle.",
mc_not_answered: "Brak odpowiedzi. Poprawna odpowiedź:",
mc_result: "z",
mc_result_correct: "poprawnie",
mc_percent: "poprawnie",
mc_no_questions: "Pytania dla tej karty jeszcze nie zostały utworzone.",
mc_print: "Drukuj",
mc_print_with_answers: "Drukować z odpowiedziami?",
cloze_title: "Tekst z lukami",
cloze_desc: "Tworzy teksty z wieloma lukami w każdym zdaniu. W tym tłumaczenie dla rodziców.",
cloze_translation: "Tłumaczenie:",
cloze_generate: "Utwórz tekst",
cloze_start: "Rozpocznij ćwiczenie",
cloze_exercise_title: "Ćwiczenie z lukami",
cloze_instruction: "Wypełnij luki i kliknij 'Sprawdź'.",
cloze_check: "Sprawdź",
cloze_show_answers: "Pokaż odpowiedzi",
cloze_no_texts: "Teksty dla tej karty jeszcze nie zostały utworzone.",
cloze_sentences: "zdań",
cloze_gaps: "luk",
cloze_gaps_total: "Łącznie luk",
cloze_with_gaps: "(z lukami)",
cloze_print: "Drukuj",
cloze_print_with_answers: "Drukować z odpowiedziami?",
qa_title: "Arkusz pytań i odpowiedzi",
qa_desc: "Pary pytanie-odpowiedź z systemem Leitnera. Powtórki według poziomu trudności.",
qa_generate: "Utwórz P&O",
qa_learn: "Rozpocznij naukę",
qa_print: "Drukuj",
qa_no_questions: "P&O dla tej karty jeszcze nie zostały utworzone.",
qa_box_new: "Nowy",
qa_box_learning: "W nauce",
qa_box_mastered: "Opanowane",
qa_show_answer: "Pokaż odpowiedź",
qa_your_answer: "Twoja odpowiedź",
qa_type_answer: "Napisz swoją odpowiedź tutaj...",
qa_check_answer: "Sprawdź odpowiedź",
qa_correct_answer: "Prawidłowa odpowiedź",
qa_self_evaluate: "Czy twoja odpowiedź była poprawna?",
qa_no_answer: "(nie wprowadzono odpowiedzi)",
qa_correct: "Dobrze",
qa_incorrect: "Źle",
qa_key_terms: "Kluczowe pojęcia",
qa_session_correct: "Dobrze",
qa_session_incorrect: "Źle",
qa_session_complete: "Runda nauki zakończona!",
qa_result_correct: "poprawnie",
qa_restart: "Ucz się ponownie",
qa_print_with_answers: "Drukować z odpowiedziami?",
question: "Pytanie",
answer: "Odpowiedź",
status_generating_qa: "Tworzenie P&O…",
status_qa_generated: "P&O utworzone",
close: "Zamknij",
subject: "Przedmiot",
grade: "Poziom",
questions: "pytań",
worksheet: "Karta pracy",
loading: "Ładowanie...",
error: "Błąd",
success: "Sukces",
imprint: "Impressum",
privacy: "Prywatność",
contact: "Kontakt",
status_ready: "Gotowe",
status_processing: "Przetwarzanie...",
status_generating_mc: "Tworzenie pytań…",
status_generating_cloze: "Tworzenie tekstów…",
status_please_wait: "Proszę czekać, AI pracuje.",
status_mc_generated: "Pytania utworzone",
status_cloze_generated: "Teksty utworzone",
status_files_created: "plików utworzono",
// Mindmap Tile
mindmap_title: "Plakat Mapa myśli",
mindmap_desc: "Tworzy przyjazną dla dzieci mapę myśli z głównym tematem w centrum i wszystkimi terminami w kolorowych kategoriach.",
mindmap_generate: "Utwórz mapę",
mindmap_show: "Podgląd",
mindmap_print_a3: "Drukuj A3",
generating_mindmap: "Tworzenie mapy...",
mindmap_generated: "Mapa utworzona!",
no_analysis: "Brak analizy",
analyze_first: "Najpierw wykonaj analizę (kliknij Przetwórz)",
categories: "Kategorie",
terms: "Terminy",
},
en: {
brand_sub: "Studio",
nav_compare: "Worksheets",
nav_tiles: "Learning Tiles",
login: "Login / Sign Up",
mvp_local: "MVP · Local on your Mac",
sidebar_areas: "Areas",
sidebar_studio: "Worksheet Studio",
sidebar_active: "active",
sidebar_parents: "Parents Channel",
sidebar_soon: "coming soon",
sidebar_correction: "Correction / Grades",
sidebar_units: "Learning Units (local)",
input_student: "Student",
input_subject: "Subject",
input_grade: "Grade (e.g. 7a)",
input_unit_title: "Learning Unit / Topic",
btn_create: "Create",
btn_add_current: "Add current worksheet",
btn_filter_unit: "Unit only",
btn_filter_all: "All files",
uploaded_worksheets: "Uploaded Worksheets",
files: "files",
btn_upload: "Upload",
btn_delete: "Delete",
original_scan: "Original Scan",
cleaned_version: "Cleaned (handwriting removed)",
no_cleaned: "No cleaned version available yet.",
process_hint: "Click 'Process' to analyze and clean the worksheet.",
worksheet_print: "Print",
worksheet_no_data: "No worksheet data available.",
btn_full_process: "Process (Analysis + Cleaning + HTML)",
btn_original_generate: "Generate Original HTML Only",
learning_unit: "Learning Unit",
no_unit_selected: "No unit selected",
mc_title: "Multiple Choice Test",
mc_ready: "Ready",
mc_generating: "Generating...",
mc_done: "Done",
mc_error: "Error",
mc_desc: "Creates multiple choice questions matching the original difficulty level (e.g. Grade 7).",
mc_generate: "Generate MC",
mc_show: "Show Questions",
mc_quiz_title: "Multiple Choice Quiz",
mc_evaluate: "Evaluate",
mc_correct: "Correct!",
mc_incorrect: "Unfortunately wrong.",
mc_not_answered: "Not answered. Correct answer:",
mc_result: "of",
mc_result_correct: "correct",
mc_percent: "correct",
mc_no_questions: "No MC questions generated yet for this worksheet.",
mc_print: "Print",
mc_print_with_answers: "Print with answers?",
cloze_title: "Fill in the Blanks",
cloze_desc: "Creates texts with multiple meaningful gaps per sentence. Including translation for parents.",
cloze_translation: "Translation:",
cloze_generate: "Generate Cloze Text",
cloze_start: "Start Exercise",
cloze_exercise_title: "Fill in the Blanks Exercise",
cloze_instruction: "Fill in the blanks and click 'Check'.",
cloze_check: "Check",
cloze_show_answers: "Show Answers",
cloze_no_texts: "No cloze texts generated yet for this worksheet.",
cloze_sentences: "sentences",
cloze_gaps: "gaps",
cloze_gaps_total: "Total gaps",
cloze_with_gaps: "(with gaps)",
cloze_print: "Print",
cloze_print_with_answers: "Print with answers?",
qa_title: "Question & Answer Sheet",
qa_desc: "Q&A pairs with Leitner box system. Spaced repetition by difficulty level.",
qa_generate: "Generate Q&A",
qa_learn: "Start Learning",
qa_print: "Print",
qa_no_questions: "No Q&A generated yet for this worksheet.",
qa_box_new: "New",
qa_box_learning: "Learning",
qa_box_mastered: "Mastered",
qa_show_answer: "Show Answer",
qa_your_answer: "Your Answer",
qa_type_answer: "Write your answer here...",
qa_check_answer: "Check Answer",
qa_correct_answer: "Correct Answer",
qa_self_evaluate: "Was your answer correct?",
qa_no_answer: "(no answer entered)",
qa_correct: "Correct",
qa_incorrect: "Incorrect",
qa_key_terms: "Key Terms",
qa_session_correct: "Correct",
qa_session_incorrect: "Incorrect",
qa_session_complete: "Learning session complete!",
qa_result_correct: "correct",
qa_restart: "Learn Again",
qa_print_with_answers: "Print with answers?",
question: "Question",
answer: "Answer",
status_generating_qa: "Generating Q&A…",
status_qa_generated: "Q&A generated",
close: "Close",
subject: "Subject",
grade: "Level",
questions: "questions",
worksheet: "Worksheet",
loading: "Loading...",
error: "Error",
success: "Success",
imprint: "Imprint",
privacy: "Privacy",
contact: "Contact",
status_ready: "Ready",
status_processing: "Processing...",
status_generating_mc: "Generating MC questions…",
status_generating_cloze: "Generating cloze texts…",
status_please_wait: "Please wait, AI is working.",
status_mc_generated: "MC questions generated",
status_cloze_generated: "Cloze texts generated",
status_files_created: "files created",
// Mindmap Tile
mindmap_title: "Mindmap Learning Poster",
mindmap_desc: "Creates a child-friendly mindmap with the main topic in the center and all terms in colorful categories.",
mindmap_generate: "Create Mindmap",
mindmap_show: "View",
mindmap_print_a3: "Print A3",
generating_mindmap: "Creating mindmap...",
mindmap_generated: "Mindmap created!",
no_analysis: "No analysis",
analyze_first: "Please analyze first (click Process)",
categories: "Categories",
terms: "Terms",
}
};
// Aktuelle Sprache (aus localStorage oder Browser-Sprache)
let currentLang = localStorage.getItem('bp_language') || 'de';
// RTL-Sprachen
const rtlLanguages = ['ar'];
// Übersetzungsfunktion
function t(key) {
const lang = translations[currentLang] || translations['de'];
return lang[key] || translations['de'][key] || key;
}
// Sprache anwenden
function applyLanguage(lang) {
currentLang = lang;
localStorage.setItem('bp_language', lang);
// RTL für Arabisch
if (rtlLanguages.includes(lang)) {
document.documentElement.setAttribute('dir', 'rtl');
} else {
document.documentElement.setAttribute('dir', 'ltr');
}
// Alle Elemente mit data-i18n aktualisieren
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (el.tagName === 'INPUT' && el.hasAttribute('placeholder')) {
el.placeholder = t(key);
} else {
el.textContent = t(key);
}
});
// Spezielle Elemente manuell aktualisieren
updateUITexts();
}
// UI-Texte aktualisieren
function updateUITexts() {
// Header
const brandSub = document.querySelector('.brand-text-sub');
if (brandSub) brandSub.textContent = t('brand_sub');
// Navigation (Text entfernt - wird nicht mehr angezeigt)
// const navItems = document.querySelectorAll('.top-nav-item');
// if (navItems[0]) navItems[0].textContent = t('nav_compare');
// if (navItems[1]) navItems[1].textContent = t('nav_tiles');
// Sidebar Bereiche
const sidebarTitles = document.querySelectorAll('.sidebar-section-title');
if (sidebarTitles[0]) sidebarTitles[0].textContent = t('sidebar_areas');
if (sidebarTitles[1]) sidebarTitles[1].textContent = t('sidebar_units');
const sidebarLabels = document.querySelectorAll('.sidebar-item-label span');
if (sidebarLabels[0]) sidebarLabels[0].textContent = t('sidebar_studio');
if (sidebarLabels[1]) sidebarLabels[1].textContent = t('sidebar_parents');
if (sidebarLabels[2]) sidebarLabels[2].textContent = t('sidebar_correction');
const sidebarBadges = document.querySelectorAll('.sidebar-item-badge');
if (sidebarBadges[0]) sidebarBadges[0].textContent = t('sidebar_active');
if (sidebarBadges[1]) sidebarBadges[1].textContent = t('sidebar_soon');
if (sidebarBadges[2]) sidebarBadges[2].textContent = t('sidebar_soon');
// Input Placeholders
if (unitStudentInput) unitStudentInput.placeholder = t('input_student');
if (unitSubjectInput) unitSubjectInput.placeholder = t('input_subject');
if (unitGradeInput) unitGradeInput.placeholder = t('input_grade');
if (unitTitleInput) unitTitleInput.placeholder = t('input_unit_title');
// Buttons
if (btnAddUnit) btnAddUnit.textContent = t('btn_create');
if (btnAttachCurrentToLu) btnAttachCurrentToLu.textContent = t('btn_add_current');
if (btnToggleFilter) {
btnToggleFilter.textContent = showOnlyUnitFiles ? t('btn_filter_unit') : t('btn_filter_all');
}
if (btnFullProcess) btnFullProcess.textContent = t('btn_full_process');
if (btnOriginalGenerate) btnOriginalGenerate.textContent = t('btn_original_generate');
// Titles
const uploadedTitle = document.querySelector('.panel-left > div:first-child');
if (uploadedTitle) {
uploadedTitle.innerHTML = '<span data-i18n="uploaded_worksheets">' + t('uploaded_worksheets') + '</span> <span class="pill" id="eingang-count">0 ' + t('files') + '</span>';
}
// MC Tile
const mcTitle = document.querySelector('[data-tile="mc"] .card-title');
if (mcTitle) mcTitle.textContent = t('mc_title');
const mcDesc = document.querySelector('[data-tile="mc"] .card-body > div:first-child');
if (mcDesc) mcDesc.textContent = t('mc_desc');
if (btnMcGenerate) btnMcGenerate.textContent = t('mc_generate');
if (btnMcShow) btnMcShow.textContent = t('mc_show');
// Cloze Tile
const clozeTitle = document.querySelector('[data-tile="cloze"] .card-title');
if (clozeTitle) clozeTitle.textContent = t('cloze_title');
const clozeDesc = document.querySelector('[data-tile="cloze"] .card-body > div:first-child');
if (clozeDesc) clozeDesc.textContent = t('cloze_desc');
const clozeLabel = document.querySelector('.cloze-language-select label');
if (clozeLabel) clozeLabel.textContent = t('cloze_translation');
if (btnClozeGenerate) btnClozeGenerate.textContent = t('cloze_generate');
if (btnClozeShow) btnClozeShow.textContent = t('cloze_start');
// QA Tile
const qaTitle = document.querySelector('[data-tile="qa"] .card-title');
if (qaTitle) qaTitle.textContent = t('qa_title');
const qaDesc = document.querySelector('[data-tile="qa"] .card-body > div:first-child');
if (qaDesc) qaDesc.textContent = t('qa_desc');
const qaBadge = document.querySelector('[data-tile="qa"] .card-badge');
if (qaBadge) qaBadge.textContent = t('qa_soon');
// Modal Titles
const mcModalTitle = document.querySelector('#mc-modal .mc-modal-title');
if (mcModalTitle) mcModalTitle.textContent = t('mc_quiz_title');
const clozeModalTitle = document.querySelector('#cloze-modal .mc-modal-title');
if (clozeModalTitle) clozeModalTitle.textContent = t('cloze_exercise_title');
// Close Buttons
document.querySelectorAll('.mc-modal-close').forEach(btn => {
btn.textContent = t('close') + ' ✕';
});
if (lightboxClose) lightboxClose.textContent = t('close') + ' ✕';
// Footer
const footerLinks = document.querySelectorAll('.footer a');
if (footerLinks[0]) footerLinks[0].textContent = t('imprint');
if (footerLinks[1]) footerLinks[1].textContent = t('privacy');
if (footerLinks[2]) footerLinks[2].textContent = t('contact');
}
const eingangListEl = document.getElementById('eingang-list');
const eingangCountEl = document.getElementById('eingang-count');
const previewContainer = document.getElementById('preview-container');
const fileInput = document.getElementById('file-input');
const btnUploadInline = document.getElementById('btn-upload-inline');
const btnFullProcess = document.getElementById('btn-full-process');
const btnOriginalGenerate = document.getElementById('btn-original-generate');
const statusBar = document.getElementById('status-bar');
const statusDot = document.getElementById('status-dot');
const statusMain = document.getElementById('status-main');
const statusSub = document.getElementById('status-sub');
const panelCompare = document.getElementById('panel-compare');
const panelTiles = document.getElementById('panel-tiles');
const pagerPrev = document.getElementById('pager-prev');
const pagerNext = document.getElementById('pager-next');
const pagerLabel = document.getElementById('pager-label');
const topNavItems = document.querySelectorAll('.top-nav-item');
const lightboxEl = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const lightboxCaption = document.getElementById('lightbox-caption');
const lightboxClose = document.getElementById('lightbox-close');
const unitStudentInput = document.getElementById('unit-student');
const unitSubjectInput = document.getElementById('unit-subject');
const unitGradeInput = document.getElementById('unit-grade');
const unitTitleInput = document.getElementById('unit-title');
const unitListEl = document.getElementById('unit-list');
const btnAddUnit = document.getElementById('btn-add-unit');
const btnAttachCurrentToLu = document.getElementById('btn-attach-current-to-lu');
const unitHeading1 = document.getElementById('unit-heading-screen1');
const unitHeading2 = document.getElementById('unit-heading-screen2');
const btnToggleFilter = document.getElementById('btn-toggle-filter');
let currentSelectedFile = null;
let allEingangFiles = []; // Master-Liste aller Dateien
let eingangFiles = []; // aktuell gefilterte Ansicht
let currentIndex = 0;
let showOnlyUnitFiles = true; // Filter-Modus: true = nur Lerneinheit (Standard), false = alle
let allWorksheetPairs = {}; // Master-Mapping original -> { clean_html, clean_image }
let worksheetPairs = {}; // aktuell gefiltertes Mapping
let tileState = {
mindmap: true,
qa: true,
mc: true,
cloze: true,
};
let currentScreen = 1;
// Lerneinheiten aus dem Backend
let units = [];
let currentUnitId = null;
// --- Lightbox / Vollbild ---
function openLightbox(src, caption) {
if (!src) return;
lightboxImg.src = src;
lightboxCaption.textContent = caption || '';
lightboxEl.classList.remove('hidden');
}
function closeLightbox() {
lightboxEl.classList.add('hidden');
lightboxImg.src = '';
lightboxCaption.textContent = '';
}
lightboxClose.addEventListener('click', closeLightbox);
lightboxEl.addEventListener('click', (ev) => {
if (ev.target === lightboxEl) {
closeLightbox();
}
});
document.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') {
closeLightbox();
// Close compare view if open
const compareView = document.getElementById('version-compare-view');
if (compareView && compareView.classList.contains('active')) {
hideCompareView();
}
}
});
// --- Status-Balken ---
function setStatus(main, sub = '', state = 'idle') {
statusMain.textContent = main;
statusSub.textContent = sub;
statusDot.classList.remove('busy', 'error');
if (state === 'busy') {
statusDot.classList.add('busy');
} else if (state === 'error') {
statusDot.classList.add('error');
}
}
setStatus('Bereit', 'Lade Arbeitsblätter hoch und starte den Neuaufbau.');
// --- API-Helfer ---
async function apiFetch(url, options = {}) {
const resp = await fetch(url, options);
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
return resp.json();
}
// --- Dateien laden & rendern ---
async function loadEingangFiles() {
try {
const data = await apiFetch('/api/eingang-dateien');
allEingangFiles = data.eingang || [];
eingangFiles = allEingangFiles.slice();
currentIndex = 0;
renderEingangList();
} catch (e) {
console.error(e);
setStatus('Fehler beim Laden der Dateien', String(e), 'error');
}
}
function renderEingangList() {
eingangListEl.innerHTML = '';
if (!eingangFiles.length) {
const li = document.createElement('li');
li.className = 'file-empty';
li.textContent = 'Noch keine Dateien vorhanden.';
eingangListEl.appendChild(li);
eingangCountEl.textContent = '0 Dateien';
return;
}
eingangFiles.forEach((filename, idx) => {
const li = document.createElement('li');
li.className = 'file-item';
if (idx === currentIndex) {
li.classList.add('active');
}
const nameSpan = document.createElement('span');
nameSpan.className = 'file-item-name';
nameSpan.textContent = filename;
const actionsSpan = document.createElement('span');
actionsSpan.style.display = 'flex';
actionsSpan.style.gap = '6px';
// Button: Aus Lerneinheit entfernen
const removeFromUnitBtn = document.createElement('span');
removeFromUnitBtn.className = 'file-item-delete';
removeFromUnitBtn.textContent = '✕';
removeFromUnitBtn.title = 'Aus Lerneinheit entfernen';
removeFromUnitBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
if (!currentUnitId) {
alert('Zum Entfernen bitte zuerst eine Lerneinheit auswählen.');
return;
}
const ok = confirm('Dieses Arbeitsblatt aus der aktuellen Lerneinheit entfernen? Die Datei selbst bleibt erhalten.');
if (!ok) return;
removeWorksheetFromCurrentUnit(eingangFiles[idx]);
});
// Button: Datei komplett löschen
const deleteFileBtn = document.createElement('span');
deleteFileBtn.className = 'file-item-delete';
deleteFileBtn.textContent = '🗑️';
deleteFileBtn.title = 'Datei komplett löschen';
deleteFileBtn.style.color = '#ef4444';
deleteFileBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const ok = confirm(`Datei "${eingangFiles[idx]}" wirklich komplett löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
if (!ok) return;
await deleteFileCompletely(eingangFiles[idx]);
});
actionsSpan.appendChild(removeFromUnitBtn);
actionsSpan.appendChild(deleteFileBtn);
li.appendChild(nameSpan);
li.appendChild(actionsSpan);
li.addEventListener('click', () => {
currentIndex = idx;
currentSelectedFile = filename;
renderEingangList();
renderPreviewForCurrent();
});
eingangListEl.appendChild(li);
});
eingangCountEl.textContent = eingangFiles.length + (eingangFiles.length === 1 ? ' Datei' : ' Dateien');
}
async function loadWorksheetPairs() {
try {
const data = await apiFetch('/api/worksheet-pairs');
allWorksheetPairs = {};
(data.pairs || []).forEach((p) => {
allWorksheetPairs[p.original] = { clean_html: p.clean_html, clean_image: p.clean_image };
});
worksheetPairs = { ...allWorksheetPairs };
renderPreviewForCurrent();
} catch (e) {
console.error(e);
setStatus('Fehler beim Laden der Neuaufbau-Daten', String(e), 'error');
}
}
function renderPreviewForCurrent() {
if (!eingangFiles.length) {
const message = showOnlyUnitFiles && currentUnitId
? 'Dieser Lerneinheit sind noch keine Arbeitsblätter zugeordnet.'
: 'Keine Dateien vorhanden.';
previewContainer.innerHTML = `<div class="preview-placeholder">${message}</div>`;
return;
}
if (currentIndex < 0) currentIndex = 0;
if (currentIndex >= eingangFiles.length) currentIndex = eingangFiles.length - 1;
const filename = eingangFiles[currentIndex];
const entry = worksheetPairs[filename] || { clean_html: null, clean_image: null };
renderPreview(entry, currentIndex);
}
function renderThumbnailsInColumn(container) {
container.innerHTML = '';
if (eingangFiles.length <= 1) {
return; // Keine Thumbnails nötig wenn nur 1 oder 0 Dateien
}
// Zeige bis zu 5 Thumbnails (die nächsten Dateien nach dem aktuellen)
const maxThumbs = 5;
let thumbCount = 0;
for (let i = 0; i < eingangFiles.length && thumbCount < maxThumbs; i++) {
if (i === currentIndex) continue; // Aktuelles Dokument überspringen
const filename = eingangFiles[i];
const thumb = document.createElement('div');
thumb.className = 'preview-thumb';
const img = document.createElement('img');
img.src = '/preview-file/' + encodeURIComponent(filename);
img.alt = filename;
const label = document.createElement('div');
label.className = 'preview-thumb-label';
label.textContent = `${i + 1}`;
thumb.appendChild(img);
thumb.appendChild(label);
thumb.addEventListener('click', () => {
currentIndex = i;
renderEingangList();
renderPreviewForCurrent();
});
container.appendChild(thumb);
thumbCount++;
}
}
function renderPreview(entry, index) {
previewContainer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'compare-wrapper';
// Original
const originalSection = document.createElement('div');
originalSection.className = 'compare-section';
const origHeader = document.createElement('div');
origHeader.className = 'compare-header';
origHeader.innerHTML = '<span>Original-Scan</span><span>Alt (links)</span>';
const origBody = document.createElement('div');
origBody.className = 'compare-body';
const origInner = document.createElement('div');
origInner.className = 'compare-body-inner';
const img = document.createElement('img');
img.className = 'preview-img';
const imgSrc = '/preview-file/' + encodeURIComponent(eingangFiles[index]);
img.src = imgSrc;
img.alt = 'Original ' + eingangFiles[index];
img.addEventListener('dblclick', () => openLightbox(imgSrc, eingangFiles[index]));
origInner.appendChild(img);
origBody.appendChild(origInner);
originalSection.appendChild(origHeader);
originalSection.appendChild(origBody);
// Neu aufgebaut
const cleanSection = document.createElement('div');
cleanSection.className = 'compare-section';
const cleanHeader = document.createElement('div');
cleanHeader.className = 'compare-header';
cleanHeader.innerHTML = '<span>Neu aufgebautes Arbeitsblatt</span><span style="display:flex;align-items:center;gap:8px;"><button type="button" class="btn btn-sm btn-ghost no-print" id="btn-print-worksheet" style="padding:4px 10px;font-size:11px;">🖨️ Drucken</button><span>Neu (rechts)</span></span>';
const cleanBody = document.createElement('div');
cleanBody.className = 'compare-body';
const cleanInner = document.createElement('div');
cleanInner.className = 'compare-body-inner';
// Bevorzuge bereinigtes Bild über HTML (für pixel-genaue Darstellung)
if (entry.clean_image) {
const imgClean = document.createElement('img');
imgClean.className = 'preview-img';
const cleanSrc = '/preview-clean-file/' + encodeURIComponent(entry.clean_image);
imgClean.src = cleanSrc;
imgClean.alt = 'Neu aufgebaut ' + eingangFiles[index];
imgClean.addEventListener('dblclick', () => openLightbox(cleanSrc, eingangFiles[index] + ' (neu)'));
cleanInner.appendChild(imgClean);
} else if (entry.clean_html) {
const frame = document.createElement('iframe');
frame.className = 'clean-frame';
frame.src = '/api/clean-html/' + encodeURIComponent(entry.clean_html);
frame.title = 'Neu aufgebautes Arbeitsblatt';
frame.addEventListener('dblclick', () => {
window.open('/api/clean-html/' + encodeURIComponent(entry.clean_html), '_blank');
});
cleanInner.appendChild(frame);
} else {
cleanInner.innerHTML = '<div class="preview-placeholder">Noch keine Neuaufbau-Daten vorhanden.</div>';
}
cleanBody.appendChild(cleanInner);
cleanSection.appendChild(cleanHeader);
cleanSection.appendChild(cleanBody);
// Print-Button Event-Listener
const printWorksheetBtn = cleanHeader.querySelector('#btn-print-worksheet');
if (printWorksheetBtn) {
printWorksheetBtn.addEventListener('click', () => {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) {
alert(t('worksheet_no_data') || 'Keine Arbeitsblatt-Daten vorhanden.');
return;
}
window.open('/api/print-worksheet/' + encodeURIComponent(currentFile), '_blank');
});
}
// Thumbnails in der Mitte
const thumbsColumn = document.createElement('div');
thumbsColumn.className = 'preview-thumbnails';
thumbsColumn.id = 'preview-thumbnails-middle';
renderThumbnailsInColumn(thumbsColumn);
wrapper.appendChild(originalSection);
wrapper.appendChild(thumbsColumn);
wrapper.appendChild(cleanSection);
// Navigation-Buttons hinzufügen
const navDiv = document.createElement('div');
navDiv.className = 'preview-nav';
const prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.textContent = '';
prevBtn.disabled = currentIndex === 0;
prevBtn.addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--;
renderEingangList();
renderPreviewForCurrent();
}
});
const nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.textContent = '';
nextBtn.disabled = currentIndex >= eingangFiles.length - 1;
nextBtn.addEventListener('click', () => {
if (currentIndex < eingangFiles.length - 1) {
currentIndex++;
renderEingangList();
renderPreviewForCurrent();
}
});
const positionSpan = document.createElement('span');
positionSpan.textContent = `${currentIndex + 1} von ${eingangFiles.length}`;
navDiv.appendChild(prevBtn);
navDiv.appendChild(positionSpan);
navDiv.appendChild(nextBtn);
wrapper.appendChild(navDiv);
previewContainer.appendChild(wrapper);
}
// --- Upload ---
btnUploadInline.addEventListener('click', async (ev) => {
ev.preventDefault();
ev.stopPropagation();
const files = fileInput.files;
if (!files || !files.length) {
alert('Bitte erst Dateien auswählen.');
return;
}
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
try {
setStatus('Upload läuft …', 'Dateien werden in den Ordner „Eingang“ geschrieben.', 'busy');
const resp = await fetch('/api/upload-multi', {
method: 'POST',
body: formData,
});
if (!resp.ok) {
console.error('Upload-Fehler: HTTP', resp.status);
setStatus('Fehler beim Upload', 'Serverantwort: HTTP ' + resp.status, 'error');
return;
}
setStatus('Upload abgeschlossen', 'Dateien wurden gespeichert.');
fileInput.value = '';
// Liste neu laden
await loadEingangFiles();
await loadWorksheetPairs();
} catch (e) {
console.error('Netzwerkfehler beim Upload', e);
setStatus('Netzwerkfehler beim Upload', String(e), 'error');
}
});
// --- Vollpipeline ---
async function runFullPipeline() {
try {
setStatus('Entferne Handschrift …', 'Bilder werden aufbereitet.', 'busy');
await apiFetch('/api/remove-handwriting-all', { method: 'POST' });
setStatus('Analysiere Arbeitsblätter …', 'Struktur wird erkannt.', 'busy');
await apiFetch('/api/analyze-all', { method: 'POST' });
setStatus('Erzeuge HTML-Arbeitsblätter …', 'Neuaufbau läuft.', 'busy');
await apiFetch('/api/generate-clean', { method: 'POST' });
setStatus('Fertig', 'Alt & Neu können jetzt verglichen werden.');
await loadWorksheetPairs();
renderPreviewForCurrent();
} catch (e) {
console.error(e);
setStatus('Fehler in der Verarbeitung', String(e), 'error');
}
}
if (btnFullProcess) btnFullProcess.addEventListener('click', runFullPipeline);
if (btnOriginalGenerate) btnOriginalGenerate.addEventListener('click', runFullPipeline);
// --- Screen-Navigation (oben + Pager unten) ---
function updateScreen() {
if (currentScreen === 1) {
panelCompare.style.display = 'flex';
panelTiles.style.display = 'none';
pagerLabel.textContent = '1 von 2';
} else {
panelCompare.style.display = 'none';
panelTiles.style.display = 'flex';
pagerLabel.textContent = '2 von 2';
}
topNavItems.forEach((item) => {
const screen = Number(item.getAttribute('data-screen'));
if (screen === currentScreen) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
topNavItems.forEach((item) => {
item.addEventListener('click', () => {
const screen = Number(item.getAttribute('data-screen'));
currentScreen = screen;
updateScreen();
});
});
pagerPrev.addEventListener('click', () => {
if (currentScreen > 1) {
currentScreen -= 1;
updateScreen();
}
});
pagerNext.addEventListener('click', () => {
if (currentScreen < 2) {
currentScreen += 1;
updateScreen();
}
});
// --- Toggle-Kacheln ---
const tileToggles = document.querySelectorAll('.toggle-pill');
const cards = document.querySelectorAll('.card');
function updateTiles() {
let activeTiles = Object.keys(tileState).filter((k) => tileState[k]);
cards.forEach((card) => {
const key = card.getAttribute('data-tile');
if (!tileState[key]) {
card.classList.add('card-hidden');
} else {
card.classList.remove('card-hidden');
}
card.classList.remove('card-full');
});
if (activeTiles.length === 1) {
const only = activeTiles[0];
cards.forEach((card) => {
if (card.getAttribute('data-tile') === only) {
card.classList.add('card-full');
}
});
}
}
tileToggles.forEach((btn) => {
btn.addEventListener('click', () => {
const key = btn.getAttribute('data-tile');
tileState[key] = !tileState[key];
btn.classList.toggle('active', tileState[key]);
updateTiles();
});
});
// --- Lerneinheiten-Logik (Backend) ---
function updateUnitHeading(unit = null) {
if (!unit && currentUnitId && units && units.length) {
unit = units.find((u) => u.id === currentUnitId) || null;
}
let text = 'Keine Lerneinheit ausgewählt';
if (unit) {
const name = unit.label || unit.title || 'Lerneinheit';
text = 'Lerneinheit: ' + name;
}
if (unitHeading1) unitHeading1.textContent = text;
if (unitHeading2) unitHeading2.textContent = text;
}
function applyUnitFilter() {
let unit = null;
if (currentUnitId && units && units.length) {
unit = units.find((u) => u.id === currentUnitId) || null;
}
// Wenn Filter deaktiviert ODER keine Lerneinheit ausgewählt -> alle Dateien anzeigen
if (!showOnlyUnitFiles || !unit || !Array.isArray(unit.worksheet_files) || unit.worksheet_files.length === 0) {
eingangFiles = allEingangFiles.slice();
worksheetPairs = { ...allWorksheetPairs };
currentIndex = 0;
renderEingangList();
renderPreviewForCurrent();
updateUnitHeading(unit);
return;
}
// Filter aktiv: nur Dateien der aktuellen Lerneinheit anzeigen
const allowed = new Set(unit.worksheet_files || []);
eingangFiles = allEingangFiles.filter((f) => allowed.has(f));
const filteredPairs = {};
Object.keys(allWorksheetPairs).forEach((key) => {
if (allowed.has(key)) {
filteredPairs[key] = allWorksheetPairs[key];
}
});
worksheetPairs = filteredPairs;
currentIndex = 0;
renderEingangList();
renderPreviewForCurrent();
updateUnitHeading(unit);
}
async function loadLearningUnits() {
try {
const resp = await fetch('/api/learning-units/');
if (!resp.ok) {
console.error('Fehler beim Laden der Lerneinheiten', resp.status);
return;
}
units = await resp.json();
if (units.length && !currentUnitId) {
currentUnitId = units[0].id;
}
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Laden der Lerneinheiten', e);
}
}
function renderUnits() {
unitListEl.innerHTML = '';
if (!units.length) {
const li = document.createElement('li');
li.className = 'unit-item';
li.textContent = 'Noch keine Lerneinheiten angelegt.';
unitListEl.appendChild(li);
updateUnitHeading(null);
return;
}
units.forEach((u) => {
const li = document.createElement('li');
li.className = 'unit-item';
if (u.id === currentUnitId) {
li.classList.add('active');
}
const contentDiv = document.createElement('div');
contentDiv.style.flex = '1';
contentDiv.style.minWidth = '0';
const titleEl = document.createElement('div');
titleEl.textContent = u.label || u.title || 'Lerneinheit';
const metaEl = document.createElement('div');
metaEl.className = 'unit-item-meta';
const metaParts = [];
if (u.meta) {
metaParts.push(u.meta);
}
if (Array.isArray(u.worksheet_files)) {
metaParts.push('Blätter: ' + u.worksheet_files.length);
}
metaEl.textContent = metaParts.join(' · ');
contentDiv.appendChild(titleEl);
contentDiv.appendChild(metaEl);
// Delete-Button
const deleteBtn = document.createElement('span');
deleteBtn.textContent = '🗑️';
deleteBtn.style.cursor = 'pointer';
deleteBtn.style.fontSize = '12px';
deleteBtn.style.color = '#ef4444';
deleteBtn.title = 'Lerneinheit löschen';
deleteBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const ok = confirm(`Lerneinheit "${u.label || u.title}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
if (!ok) return;
await deleteLearningUnit(u.id);
});
li.appendChild(contentDiv);
li.appendChild(deleteBtn);
li.addEventListener('click', () => {
currentUnitId = u.id;
renderUnits();
applyUnitFilter();
});
unitListEl.appendChild(li);
});
}
async function addUnitFromForm() {
const student = (unitStudentInput.value || '').trim();
const subject = (unitSubjectInput.value || '').trim();
const grade = (unitGradeInput && unitGradeInput.value || '').trim();
const title = (unitTitleInput.value || '').trim();
if (!student && !subject && !title) {
alert('Bitte mindestens einen Wert (Schüler/in, Fach oder Thema) eintragen.');
return;
}
const payload = {
student,
subject,
title,
grade,
};
try {
const resp = await fetch('/api/learning-units/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Anlegen der Lerneinheit', resp.status);
alert('Lerneinheit konnte nicht angelegt werden.');
return;
}
const created = await resp.json();
units.push(created);
currentUnitId = created.id;
unitStudentInput.value = '';
unitSubjectInput.value = '';
unitTitleInput.value = '';
if (unitGradeInput) unitGradeInput.value = '';
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Anlegen der Lerneinheit', e);
alert('Netzwerkfehler beim Anlegen der Lerneinheit.');
}
}
function getCurrentWorksheetBasename() {
if (!eingangFiles.length) return null;
if (currentIndex < 0 || currentIndex >= eingangFiles.length) return null;
return eingangFiles[currentIndex];
}
async function attachCurrentWorksheetToUnit() {
if (!currentUnitId) {
alert('Bitte zuerst eine Lerneinheit auswählen oder anlegen.');
return;
}
const basename = getCurrentWorksheetBasename();
if (!basename) {
alert('Bitte zuerst ein Arbeitsblatt im linken Bereich auswählen.');
return;
}
const payload = { worksheet_files: [basename] };
try {
const resp = await fetch(`/api/learning-units/${currentUnitId}/attach-worksheets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Zuordnen des Arbeitsblatts', resp.status);
alert('Arbeitsblatt konnte nicht zugeordnet werden.');
return;
}
const updated = await resp.json();
const idx = units.findIndex((u) => u.id === updated.id);
if (idx !== -1) {
units[idx] = updated;
}
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Zuordnen des Arbeitsblatts', e);
alert('Netzwerkfehler beim Zuordnen des Arbeitsblatts.');
}
}
async function removeWorksheetFromCurrentUnit(filename) {
if (!currentUnitId) {
alert('Bitte zuerst eine Lerneinheit auswählen.');
return;
}
if (!filename) {
alert('Fehler: kein Dateiname übergeben.');
return;
}
const payload = { worksheet_file: filename };
try {
const resp = await fetch(`/api/learning-units/${currentUnitId}/remove-worksheet`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
console.error('Fehler beim Entfernen des Arbeitsblatts', resp.status);
alert('Arbeitsblatt konnte nicht aus der Lerneinheit entfernt werden.');
return;
}
const updated = await resp.json();
const idx = units.findIndex((u) => u.id === updated.id);
if (idx !== -1) {
units[idx] = updated;
}
renderUnits();
applyUnitFilter();
} catch (e) {
console.error('Netzwerkfehler beim Entfernen des Arbeitsblatts', e);
alert('Netzwerkfehler beim Entfernen des Arbeitsblatts.');
}
}
async function deleteFileCompletely(filename) {
if (!filename) {
alert('Fehler: kein Dateiname übergeben.');
return;
}
try {
setStatus('Lösche Datei …', filename, 'busy');
const resp = await fetch(`/api/eingang-dateien/${encodeURIComponent(filename)}`, {
method: 'DELETE',
});
if (!resp.ok) {
console.error('Fehler beim Löschen der Datei', resp.status);
setStatus('Fehler beim Löschen', filename, 'error');
alert('Datei konnte nicht gelöscht werden.');
return;
}
const result = await resp.json();
if (result.status === 'OK') {
setStatus('Datei gelöscht', filename);
// Dateien neu laden
await loadEingangFiles();
await loadWorksheetPairs();
await loadLearningUnits();
} else {
setStatus('Fehler', result.message, 'error');
alert(result.message);
}
} catch (e) {
console.error('Netzwerkfehler beim Löschen der Datei', e);
setStatus('Netzwerkfehler', String(e), 'error');
alert('Netzwerkfehler beim Löschen der Datei.');
}
}
async function deleteLearningUnit(unitId) {
if (!unitId) {
alert('Fehler: keine Lerneinheit-ID übergeben.');
return;
}
try {
setStatus('Lösche Lerneinheit …', '', 'busy');
const resp = await fetch(`/api/learning-units/${unitId}`, {
method: 'DELETE',
});
if (!resp.ok) {
console.error('Fehler beim Löschen der Lerneinheit', resp.status);
setStatus('Fehler beim Löschen', '', 'error');
alert('Lerneinheit konnte nicht gelöscht werden.');
return;
}
const result = await resp.json();
if (result.status === 'deleted') {
setStatus('Lerneinheit gelöscht', '');
// Lerneinheit aus der lokalen Liste entfernen
units = units.filter((u) => u.id !== unitId);
// Wenn die gelöschte Einheit ausgewählt war, Auswahl zurücksetzen
if (currentUnitId === unitId) {
currentUnitId = units.length > 0 ? units[0].id : null;
}
renderUnits();
applyUnitFilter();
} else {
setStatus('Fehler', 'Unbekannter Fehler', 'error');
alert('Fehler beim Löschen der Lerneinheit.');
}
} catch (e) {
console.error('Netzwerkfehler beim Löschen der Lerneinheit', e);
setStatus('Netzwerkfehler', String(e), 'error');
alert('Netzwerkfehler beim Löschen der Lerneinheit.');
}
}
if (btnAddUnit) {
btnAddUnit.addEventListener('click', (ev) => {
ev.preventDefault();
addUnitFromForm();
});
}
if (btnAttachCurrentToLu) {
btnAttachCurrentToLu.addEventListener('click', (ev) => {
ev.preventDefault();
attachCurrentWorksheetToUnit();
});
}
// --- Filter-Toggle ---
if (btnToggleFilter) {
btnToggleFilter.addEventListener('click', () => {
showOnlyUnitFiles = !showOnlyUnitFiles;
if (showOnlyUnitFiles) {
btnToggleFilter.textContent = 'Nur Lerneinheit';
btnToggleFilter.classList.add('btn-primary');
} else {
btnToggleFilter.textContent = 'Alle Dateien';
btnToggleFilter.classList.remove('btn-primary');
}
applyUnitFilter();
});
}
// --- Multiple Choice Logik ---
const btnMcGenerate = document.getElementById('btn-mc-generate');
const btnMcShow = document.getElementById('btn-mc-show');
const btnMcPrint = document.getElementById('btn-mc-print');
const mcPreview = document.getElementById('mc-preview');
const mcBadge = document.getElementById('mc-badge');
const mcModal = document.getElementById('mc-modal');
const mcModalBody = document.getElementById('mc-modal-body');
const mcModalClose = document.getElementById('mc-modal-close');
let currentMcData = null;
let mcAnswers = {}; // Speichert Nutzerantworten
async function generateMcQuestions() {
try {
setStatus('Generiere MC-Fragen …', 'Bitte warten, KI arbeitet.', 'busy');
if (mcBadge) mcBadge.textContent = 'Generiert...';
const resp = await fetch('/api/generate-mc', { method: 'POST' });
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const result = await resp.json();
if (result.status === 'OK' && result.generated.length > 0) {
setStatus('MC-Fragen generiert', result.generated.length + ' Dateien erstellt.');
if (mcBadge) mcBadge.textContent = 'Fertig';
if (btnMcShow) btnMcShow.style.display = 'inline-block';
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
// Lade die erste MC-Datei für Vorschau
await loadMcPreviewForCurrent();
} else if (result.errors && result.errors.length > 0) {
setStatus('Fehler bei MC-Generierung', result.errors[0].error, 'error');
if (mcBadge) mcBadge.textContent = 'Fehler';
} else {
setStatus('Keine MC-Fragen generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
if (mcBadge) mcBadge.textContent = 'Bereit';
}
} catch (e) {
console.error('MC-Generierung fehlgeschlagen:', e);
setStatus('Fehler bei MC-Generierung', String(e), 'error');
if (mcBadge) mcBadge.textContent = 'Fehler';
}
}
async function loadMcPreviewForCurrent() {
if (!eingangFiles.length) {
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/mc-data/' + encodeURIComponent(currentFile));
const result = await resp.json();
if (result.status === 'OK' && result.data) {
currentMcData = result.data;
renderMcPreview(result.data);
if (btnMcShow) btnMcShow.style.display = 'inline-block';
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
} else {
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine MC-Fragen für dieses Arbeitsblatt generiert.</div>';
currentMcData = null;
if (btnMcPrint) btnMcPrint.style.display = 'none';
}
} catch (e) {
console.error('Fehler beim Laden der MC-Daten:', e);
if (mcPreview) mcPreview.innerHTML = '';
}
}
function renderMcPreview(mcData) {
if (!mcPreview) return;
if (!mcData || !mcData.questions || mcData.questions.length === 0) {
mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Fragen vorhanden.</div>';
return;
}
const questions = mcData.questions;
const metadata = mcData.metadata || {};
let html = '';
// Zeige Metadaten
if (metadata.grade_level || metadata.subject) {
html += '<div class="mc-stats">';
if (metadata.subject) {
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
}
if (metadata.grade_level) {
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
}
html += '<div class="mc-stats-item"><strong>Fragen:</strong> ' + questions.length + '</div>';
html += '</div>';
}
// Zeige erste 2 Fragen als Vorschau
const previewQuestions = questions.slice(0, 2);
previewQuestions.forEach((q, idx) => {
html += '<div class="mc-question">';
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
html += '<div class="mc-options">';
q.options.forEach(opt => {
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
html += '</div>';
});
html += '</div>';
html += '</div>';
});
if (questions.length > 2) {
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (questions.length - 2) + ' weitere Fragen</div>';
}
mcPreview.innerHTML = html;
// Event-Listener für Antwort-Auswahl
mcPreview.querySelectorAll('.mc-option').forEach(optEl => {
optEl.addEventListener('click', () => handleMcOptionClick(optEl));
});
}
function handleMcOptionClick(optEl) {
const qid = optEl.getAttribute('data-qid');
const optId = optEl.getAttribute('data-opt');
if (!currentMcData) return;
// Finde die Frage
const question = currentMcData.questions.find(q => q.id === qid);
if (!question) return;
// Markiere alle Optionen dieser Frage
const questionEl = optEl.closest('.mc-question');
const allOptions = questionEl.querySelectorAll('.mc-option');
allOptions.forEach(opt => {
opt.classList.remove('selected', 'correct', 'incorrect');
const thisOptId = opt.getAttribute('data-opt');
if (thisOptId === question.correct_answer) {
opt.classList.add('correct');
} else if (thisOptId === optId) {
opt.classList.add('incorrect');
}
});
// Speichere Antwort
mcAnswers[qid] = optId;
// Zeige Feedback wenn gewünscht
const isCorrect = optId === question.correct_answer;
let feedbackEl = questionEl.querySelector('.mc-feedback');
if (!feedbackEl) {
feedbackEl = document.createElement('div');
feedbackEl.className = 'mc-feedback';
questionEl.appendChild(feedbackEl);
}
if (isCorrect) {
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
feedbackEl.style.color = 'var(--bp-accent)';
feedbackEl.textContent = 'Richtig! ' + (question.explanation || '');
} else {
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
feedbackEl.style.color = '#ef4444';
feedbackEl.textContent = 'Leider falsch. ' + (question.explanation || '');
}
}
function openMcModal() {
if (!currentMcData || !currentMcData.questions) {
alert('Keine MC-Fragen vorhanden. Bitte zuerst generieren.');
return;
}
mcAnswers = {}; // Reset Antworten
renderMcModal(currentMcData);
mcModal.classList.remove('hidden');
}
function closeMcModal() {
mcModal.classList.add('hidden');
}
function renderMcModal(mcData) {
const questions = mcData.questions;
const metadata = mcData.metadata || {};
let html = '';
// Header mit Metadaten
html += '<div class="mc-stats" style="margin-bottom:16px;">';
if (metadata.source_title) {
html += '<div class="mc-stats-item"><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
}
if (metadata.subject) {
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
}
if (metadata.grade_level) {
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
}
html += '</div>';
// Alle Fragen
questions.forEach((q, idx) => {
html += '<div class="mc-question" data-qid="' + q.id + '">';
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
html += '<div class="mc-options">';
q.options.forEach(opt => {
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
html += '</div>';
});
html += '</div>';
html += '</div>';
});
// Auswertungs-Button
html += '<div style="margin-top:16px;text-align:center;">';
html += '<button class="btn btn-primary" id="btn-mc-evaluate">Auswerten</button>';
html += '</div>';
mcModalBody.innerHTML = html;
// Event-Listener
mcModalBody.querySelectorAll('.mc-option').forEach(optEl => {
optEl.addEventListener('click', () => {
const qid = optEl.getAttribute('data-qid');
const optId = optEl.getAttribute('data-opt');
// Deselektiere andere Optionen der gleichen Frage
const questionEl = optEl.closest('.mc-question');
questionEl.querySelectorAll('.mc-option').forEach(o => o.classList.remove('selected'));
optEl.classList.add('selected');
mcAnswers[qid] = optId;
});
});
const btnEvaluate = document.getElementById('btn-mc-evaluate');
if (btnEvaluate) {
btnEvaluate.addEventListener('click', evaluateMcQuiz);
}
}
function evaluateMcQuiz() {
if (!currentMcData) return;
let correct = 0;
let total = currentMcData.questions.length;
currentMcData.questions.forEach(q => {
const questionEl = mcModalBody.querySelector('.mc-question[data-qid="' + q.id + '"]');
if (!questionEl) return;
const userAnswer = mcAnswers[q.id];
const allOptions = questionEl.querySelectorAll('.mc-option');
allOptions.forEach(opt => {
opt.classList.remove('correct', 'incorrect');
const optId = opt.getAttribute('data-opt');
if (optId === q.correct_answer) {
opt.classList.add('correct');
} else if (optId === userAnswer && userAnswer !== q.correct_answer) {
opt.classList.add('incorrect');
}
});
// Zeige Erklärung
let feedbackEl = questionEl.querySelector('.mc-feedback');
if (!feedbackEl) {
feedbackEl = document.createElement('div');
feedbackEl.className = 'mc-feedback';
questionEl.appendChild(feedbackEl);
}
if (userAnswer === q.correct_answer) {
correct++;
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
feedbackEl.style.color = 'var(--bp-accent)';
feedbackEl.textContent = 'Richtig! ' + (q.explanation || '');
} else if (userAnswer) {
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
feedbackEl.style.color = '#ef4444';
feedbackEl.textContent = 'Falsch. ' + (q.explanation || '');
} else {
feedbackEl.style.background = 'rgba(148,163,184,0.1)';
feedbackEl.style.borderColor = 'rgba(148,163,184,0.3)';
feedbackEl.style.color = 'var(--bp-text-muted)';
feedbackEl.textContent = 'Nicht beantwortet. Richtig wäre: ' + q.correct_answer.toUpperCase();
}
});
// Zeige Gesamtergebnis
const resultHtml = '<div style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
'</div>';
const existingResult = mcModalBody.querySelector('.mc-result');
if (existingResult) {
existingResult.remove();
}
const resultDiv = document.createElement('div');
resultDiv.className = 'mc-result';
resultDiv.innerHTML = resultHtml;
mcModalBody.appendChild(resultDiv);
}
function openMcPrintDialog() {
if (!currentMcData) {
alert(t('mc_no_questions') || 'Keine MC-Fragen vorhanden.');
return;
}
const currentFile = eingangFiles[currentIndex];
const choice = confirm((t('mc_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Lösungsblatt mit markierten Antworten\\nAbbrechen = Übungsblatt ohne Lösungen');
const url = '/api/print-mc/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
window.open(url, '_blank');
}
// Event Listener für MC-Buttons
if (btnMcGenerate) {
btnMcGenerate.addEventListener('click', generateMcQuestions);
}
if (btnMcShow) {
btnMcShow.addEventListener('click', openMcModal);
}
if (btnMcPrint) {
btnMcPrint.addEventListener('click', openMcPrintDialog);
}
if (mcModalClose) {
mcModalClose.addEventListener('click', closeMcModal);
}
if (mcModal) {
mcModal.addEventListener('click', (ev) => {
if (ev.target === mcModal) {
closeMcModal();
}
});
}
// --- Lückentext (Cloze) Logik ---
const btnClozeGenerate = document.getElementById('btn-cloze-generate');
const btnClozeShow = document.getElementById('btn-cloze-show');
const btnClozePrint = document.getElementById('btn-cloze-print');
const clozePreview = document.getElementById('cloze-preview');
const clozeBadge = document.getElementById('cloze-badge');
const clozeLanguageSelect = document.getElementById('cloze-language');
const clozeModal = document.getElementById('cloze-modal');
const clozeModalBody = document.getElementById('cloze-modal-body');
const clozeModalClose = document.getElementById('cloze-modal-close');
let currentClozeData = null;
let clozeAnswers = {}; // Speichert Nutzerantworten
async function generateClozeTexts() {
const targetLang = clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
try {
setStatus('Generiere Lückentexte …', 'Bitte warten, KI arbeitet.', 'busy');
if (clozeBadge) clozeBadge.textContent = 'Generiert...';
const resp = await fetch('/api/generate-cloze?target_language=' + targetLang, { method: 'POST' });
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const result = await resp.json();
if (result.status === 'OK' && result.generated.length > 0) {
setStatus('Lückentexte generiert', result.generated.length + ' Dateien erstellt.');
if (clozeBadge) clozeBadge.textContent = 'Fertig';
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
// Lade Vorschau für aktuelle Datei
await loadClozePreviewForCurrent();
} else if (result.errors && result.errors.length > 0) {
setStatus('Fehler bei Lückentext-Generierung', result.errors[0].error, 'error');
if (clozeBadge) clozeBadge.textContent = 'Fehler';
} else {
setStatus('Keine Lückentexte generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
if (clozeBadge) clozeBadge.textContent = 'Bereit';
}
} catch (e) {
console.error('Lückentext-Generierung fehlgeschlagen:', e);
setStatus('Fehler bei Lückentext-Generierung', String(e), 'error');
if (clozeBadge) clozeBadge.textContent = 'Fehler';
}
}
async function loadClozePreviewForCurrent() {
if (!eingangFiles.length) {
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/cloze-data/' + encodeURIComponent(currentFile));
const result = await resp.json();
if (result.status === 'OK' && result.data) {
currentClozeData = result.data;
renderClozePreview(result.data);
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
} else {
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine Lückentexte für dieses Arbeitsblatt generiert.</div>';
currentClozeData = null;
if (btnClozeShow) btnClozeShow.style.display = 'none';
if (btnClozePrint) btnClozePrint.style.display = 'none';
}
} catch (e) {
console.error('Fehler beim Laden der Lückentext-Daten:', e);
if (clozePreview) clozePreview.innerHTML = '';
}
}
function renderClozePreview(clozeData) {
if (!clozePreview) return;
if (!clozeData || !clozeData.cloze_items || clozeData.cloze_items.length === 0) {
clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Lückentexte vorhanden.</div>';
return;
}
const items = clozeData.cloze_items;
const metadata = clozeData.metadata || {};
let html = '';
// Statistiken
html += '<div class="cloze-stats">';
if (metadata.subject) {
html += '<div><strong>Fach:</strong> ' + metadata.subject + '</div>';
}
if (metadata.grade_level) {
html += '<div><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
}
html += '<div><strong>Sätze:</strong> ' + items.length + '</div>';
if (metadata.total_gaps) {
html += '<div><strong>Lücken:</strong> ' + metadata.total_gaps + '</div>';
}
html += '</div>';
// Zeige erste 2 Sätze als Vorschau
const previewItems = items.slice(0, 2);
previewItems.forEach((item, idx) => {
html += '<div class="cloze-item">';
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + item.sentence_with_gaps.replace(/___/g, '<span class="cloze-gap">___</span>') + '</div>';
// Übersetzung anzeigen
if (item.translation && item.translation.full_sentence) {
html += '<div class="cloze-translation">';
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ':</div>';
html += item.translation.full_sentence;
html += '</div>';
}
html += '</div>';
});
if (items.length > 2) {
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (items.length - 2) + ' weitere Sätze</div>';
}
clozePreview.innerHTML = html;
}
function openClozeModal() {
if (!currentClozeData || !currentClozeData.cloze_items) {
alert('Keine Lückentexte vorhanden. Bitte zuerst generieren.');
return;
}
clozeAnswers = {}; // Reset Antworten
renderClozeModal(currentClozeData);
clozeModal.classList.remove('hidden');
}
function closeClozeModal() {
clozeModal.classList.add('hidden');
}
function renderClozeModal(clozeData) {
const items = clozeData.cloze_items;
const metadata = clozeData.metadata || {};
let html = '';
// Header
html += '<div class="cloze-stats" style="margin-bottom:16px;">';
if (metadata.source_title) {
html += '<div><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
}
if (metadata.total_gaps) {
html += '<div><strong>Lücken gesamt:</strong> ' + metadata.total_gaps + '</div>';
}
html += '</div>';
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">Fülle die Lücken aus und klicke auf "Prüfen".</div>';
// Alle Sätze mit Eingabefeldern
items.forEach((item, idx) => {
html += '<div class="cloze-item" data-cid="' + item.id + '">';
// Satz mit Eingabefeldern statt ___
let sentenceHtml = item.sentence_with_gaps;
const gaps = item.gaps || [];
// Ersetze ___ durch Eingabefelder
let gapIndex = 0;
sentenceHtml = sentenceHtml.replace(/___/g, () => {
const gap = gaps[gapIndex] || { id: 'g' + gapIndex, word: '' };
const inputId = item.id + '_' + gap.id;
gapIndex++;
return '<input type="text" class="cloze-gap-input" data-cid="' + item.id + '" data-gid="' + gap.id + '" data-answer="' + gap.word + '" id="input_' + inputId + '" autocomplete="off">';
});
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + sentenceHtml + '</div>';
// Übersetzung als Hilfe
if (item.translation && item.translation.sentence_with_gaps) {
html += '<div class="cloze-translation">';
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ' (mit Lücken):</div>';
html += item.translation.sentence_with_gaps;
html += '</div>';
}
html += '</div>';
});
// Buttons
html += '<div style="margin-top:16px;text-align:center;display:flex;gap:8px;justify-content:center;">';
html += '<button class="btn btn-primary" id="btn-cloze-check">Prüfen</button>';
html += '<button class="btn btn-ghost" id="btn-cloze-show-answers">Lösungen zeigen</button>';
html += '</div>';
clozeModalBody.innerHTML = html;
// Event-Listener für Prüfen-Button
const btnCheck = document.getElementById('btn-cloze-check');
if (btnCheck) {
btnCheck.addEventListener('click', checkClozeAnswers);
}
// Event-Listener für Lösungen zeigen
const btnShowAnswers = document.getElementById('btn-cloze-show-answers');
if (btnShowAnswers) {
btnShowAnswers.addEventListener('click', showClozeAnswers);
}
// Enter-Taste zum Prüfen
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
checkClozeAnswers();
}
});
});
}
function checkClozeAnswers() {
let correct = 0;
let total = 0;
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
const userAnswer = input.value.trim().toLowerCase();
const correctAnswer = input.getAttribute('data-answer').toLowerCase();
total++;
// Entferne vorherige Klassen
input.classList.remove('correct', 'incorrect');
if (userAnswer === correctAnswer) {
input.classList.add('correct');
correct++;
} else if (userAnswer !== '') {
input.classList.add('incorrect');
}
});
// Zeige Ergebnis
let existingResult = clozeModalBody.querySelector('.cloze-result');
if (existingResult) existingResult.remove();
const resultHtml = '<div class="cloze-result" style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
'</div>';
const resultDiv = document.createElement('div');
resultDiv.innerHTML = resultHtml;
clozeModalBody.appendChild(resultDiv.firstChild);
}
function showClozeAnswers() {
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
const correctAnswer = input.getAttribute('data-answer');
input.value = correctAnswer;
input.classList.remove('incorrect');
input.classList.add('correct');
});
}
function openClozePrintDialog() {
if (!currentClozeData) {
alert(t('cloze_no_texts') || 'Keine Lückentexte vorhanden.');
return;
}
const currentFile = eingangFiles[currentIndex];
// Öffne Druck-Optionen
const choice = confirm((t('cloze_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit ausgefüllten Lücken\\nAbbrechen = Übungsblatt mit Wortbank');
const url = '/api/print-cloze/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
window.open(url, '_blank');
}
// Event Listener für Cloze-Buttons
if (btnClozeGenerate) {
btnClozeGenerate.addEventListener('click', generateClozeTexts);
}
if (btnClozeShow) {
btnClozeShow.addEventListener('click', openClozeModal);
}
if (btnClozePrint) {
btnClozePrint.addEventListener('click', openClozePrintDialog);
}
if (clozeModalClose) {
clozeModalClose.addEventListener('click', closeClozeModal);
}
if (clozeModal) {
clozeModal.addEventListener('click', (ev) => {
if (ev.target === clozeModal) {
closeClozeModal();
}
});
}
// --- Mindmap Lernposter Logik ---
const btnMindmapGenerate = document.getElementById('btn-mindmap-generate');
const btnMindmapShow = document.getElementById('btn-mindmap-show');
const btnMindmapPrint = document.getElementById('btn-mindmap-print');
const mindmapPreview = document.getElementById('mindmap-preview');
const mindmapBadge = document.getElementById('mindmap-badge');
let currentMindmapData = null;
async function generateMindmap() {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) {
setStatus('error', t('select_file_first') || 'Bitte zuerst eine Datei auswählen');
return;
}
if (mindmapBadge) {
mindmapBadge.textContent = t('generating') || 'Generiere...';
mindmapBadge.className = 'card-badge badge-working';
}
setStatus('working', t('generating_mindmap') || 'Erstelle Mindmap...');
try {
const resp = await fetch('/api/generate-mindmap/' + encodeURIComponent(currentFile), {
method: 'POST'
});
const data = await resp.json();
if (data.status === 'OK') {
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Fertig';
mindmapBadge.className = 'card-badge badge-success';
}
setStatus('ok', t('mindmap_generated') || 'Mindmap erstellt!');
// Lade Mindmap-Daten
await loadMindmapData();
} else if (data.status === 'NOT_FOUND') {
if (mindmapBadge) {
mindmapBadge.textContent = t('no_analysis') || 'Keine Analyse';
mindmapBadge.className = 'card-badge badge-error';
}
setStatus('error', t('analyze_first') || 'Bitte zuerst analysieren (Neuaufbau starten)');
} else {
throw new Error(data.message || 'Fehler bei der Mindmap-Generierung');
}
} catch (err) {
console.error('Mindmap error:', err);
if (mindmapBadge) {
mindmapBadge.textContent = t('error') || 'Fehler';
mindmapBadge.className = 'card-badge badge-error';
}
setStatus('error', err.message);
}
}
async function loadMindmapData() {
if (!eingangFiles.length) {
if (mindmapPreview) mindmapPreview.innerHTML = '';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/mindmap-data/' + encodeURIComponent(currentFile));
const data = await resp.json();
if (data.status === 'OK' && data.data) {
currentMindmapData = data.data;
renderMindmapPreview();
if (btnMindmapShow) btnMindmapShow.style.display = 'inline-block';
if (btnMindmapPrint) btnMindmapPrint.style.display = 'inline-block';
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Fertig';
mindmapBadge.className = 'card-badge badge-success';
}
} else {
currentMindmapData = null;
if (mindmapPreview) mindmapPreview.innerHTML = '';
if (btnMindmapShow) btnMindmapShow.style.display = 'none';
if (btnMindmapPrint) btnMindmapPrint.style.display = 'none';
if (mindmapBadge) {
mindmapBadge.textContent = t('ready') || 'Bereit';
mindmapBadge.className = 'card-badge';
}
}
} catch (err) {
console.error('Error loading mindmap:', err);
}
}
function renderMindmapPreview() {
if (!mindmapPreview) return;
if (!currentMindmapData) {
mindmapPreview.innerHTML = '';
return;
}
const topic = currentMindmapData.topic || 'Thema';
const categories = currentMindmapData.categories || [];
const categoryCount = categories.length;
const termCount = categories.reduce((sum, cat) => sum + (cat.terms ? cat.terms.length : 0), 0);
mindmapPreview.innerHTML = '<div style="margin-top:10px;padding:12px;background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border-radius:10px;text-align:center;">' +
'<div style="font-size:18px;font-weight:bold;color:#0369a1;margin-bottom:8px;">' + topic + '</div>' +
'<div style="font-size:12px;color:#64748b;">' + categoryCount + ' ' + (t('categories') || 'Kategorien') + ' | ' + termCount + ' ' + (t('terms') || 'Begriffe') + '</div>' +
'</div>';
}
function openMindmapView() {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a4', '_blank');
}
function openMindmapPrint() {
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a3', '_blank');
}
if (btnMindmapGenerate) {
btnMindmapGenerate.addEventListener('click', generateMindmap);
}
if (btnMindmapShow) {
btnMindmapShow.addEventListener('click', openMindmapView);
}
if (btnMindmapPrint) {
btnMindmapPrint.addEventListener('click', openMindmapPrint);
}
// --- Frage-Antwort (Q&A) mit Leitner-System ---
const btnQaGenerate = document.getElementById('btn-qa-generate');
const btnQaLearn = document.getElementById('btn-qa-learn');
const btnQaPrint = document.getElementById('btn-qa-print');
const qaPreview = document.getElementById('qa-preview');
const qaBadge = document.getElementById('qa-badge');
const qaModal = document.getElementById('qa-modal');
const qaModalBody = document.getElementById('qa-modal-body');
const qaModalClose = document.getElementById('qa-modal-close');
let currentQaData = null;
let currentQaIndex = 0;
let qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
async function generateQaQuestions() {
try {
setStatus(t('status_generating_qa') || 'Generiere Q&A …', t('status_please_wait'), 'busy');
if (qaBadge) qaBadge.textContent = t('mc_generating');
const resp = await fetch('/api/generate-qa', { method: 'POST' });
if (!resp.ok) {
throw new Error('HTTP ' + resp.status);
}
const result = await resp.json();
if (result.status === 'OK' && result.generated.length > 0) {
setStatus(t('status_qa_generated') || 'Q&A generiert', result.generated.length + ' ' + t('status_files_created'));
if (qaBadge) qaBadge.textContent = t('mc_done');
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
await loadQaPreviewForCurrent();
} else if (result.errors && result.errors.length > 0) {
setStatus(t('error'), result.errors[0].error, 'error');
if (qaBadge) qaBadge.textContent = t('mc_error');
} else {
setStatus(t('error'), 'Keine Q&A generiert.', 'error');
if (qaBadge) qaBadge.textContent = t('mc_ready');
}
} catch (e) {
console.error('Q&A-Generierung fehlgeschlagen:', e);
setStatus(t('error'), String(e), 'error');
if (qaBadge) qaBadge.textContent = t('mc_error');
}
}
async function loadQaPreviewForCurrent() {
if (!eingangFiles.length) {
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
return;
}
const currentFile = eingangFiles[currentIndex];
if (!currentFile) return;
try {
const resp = await fetch('/api/qa-data/' + encodeURIComponent(currentFile));
const result = await resp.json();
if (result.status === 'OK' && result.data) {
currentQaData = result.data;
renderQaPreview(result.data);
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
} else {
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_no_questions') || 'Noch keine Q&A für dieses Arbeitsblatt generiert.') + '</div>';
currentQaData = null;
if (btnQaLearn) btnQaLearn.style.display = 'none';
if (btnQaPrint) btnQaPrint.style.display = 'none';
}
} catch (e) {
console.error('Fehler beim Laden der Q&A-Daten:', e);
if (qaPreview) qaPreview.innerHTML = '';
}
}
function renderQaPreview(qaData) {
if (!qaPreview) return;
if (!qaData || !qaData.qa_items || qaData.qa_items.length === 0) {
qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
return;
}
const items = qaData.qa_items;
const metadata = qaData.metadata || {};
let html = '';
// Leitner-Box Statistiken
html += '<div class="mc-stats" style="margin-bottom:8px;">';
// Zähle Fragen nach Box
let box0 = 0, box1 = 0, box2 = 0;
items.forEach(item => {
const box = item.leitner ? item.leitner.box : 0;
if (box === 0) box0++;
else if (box === 1) box1++;
else box2++;
});
html += '<div style="display:flex;gap:12px;font-size:11px;">';
html += '<div style="color:#ef4444;">' + (t('qa_box_new') || 'Neu') + ': ' + box0 + '</div>';
html += '<div style="color:#f59e0b;">' + (t('qa_box_learning') || 'Lernt') + ': ' + box1 + '</div>';
html += '<div style="color:#22c55e;">' + (t('qa_box_mastered') || 'Gefestigt') + ': ' + box2 + '</div>';
html += '</div>';
html += '</div>';
// Zeige erste 2 Fragen als Vorschau
const previewItems = items.slice(0, 2);
previewItems.forEach((item, idx) => {
html += '<div class="mc-question" style="padding:8px;margin-bottom:6px;background:rgba(255,255,255,0.03);border-radius:6px;">';
html += '<div style="font-size:12px;font-weight:500;margin-bottom:4px;">' + (idx + 1) + '. ' + item.question + '</div>';
html += '<div style="font-size:11px;color:var(--bp-text-muted);">→ ' + item.answer.substring(0, 60) + (item.answer.length > 60 ? '...' : '') + '</div>';
html += '</div>';
});
if (items.length > 2) {
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:4px;">+ ' + (items.length - 2) + ' ' + (t('questions') || 'weitere Fragen') + '</div>';
}
qaPreview.innerHTML = html;
}
function openQaModal() {
if (!currentQaData || !currentQaData.qa_items) {
alert(t('qa_no_questions') || 'Keine Q&A vorhanden. Bitte zuerst generieren.');
return;
}
currentQaIndex = 0;
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
renderQaLearningCard();
qaModal.classList.remove('hidden');
}
function closeQaModal() {
qaModal.classList.add('hidden');
}
function renderQaLearningCard() {
const items = currentQaData.qa_items;
if (currentQaIndex >= items.length) {
// Alle Fragen durch - Zeige Zusammenfassung
renderQaSessionSummary();
return;
}
const item = items[currentQaIndex];
const leitner = item.leitner || { box: 0 };
const boxNames = [t('qa_box_new') || 'Neu', t('qa_box_learning') || 'Gelernt', t('qa_box_mastered') || 'Gefestigt'];
const boxColors = ['#ef4444', '#f59e0b', '#22c55e'];
let html = '';
// Fortschrittsanzeige
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">';
html += '<div style="font-size:12px;color:var(--bp-text-muted);">' + (t('question') || 'Frage') + ' ' + (currentQaIndex + 1) + ' / ' + items.length + '</div>';
html += '<div style="display:flex;align-items:center;gap:6px;">';
html += '<span style="font-size:10px;color:' + boxColors[leitner.box] + ';background:' + boxColors[leitner.box] + '22;padding:2px 8px;border-radius:10px;">' + boxNames[leitner.box] + '</span>';
html += '</div>';
html += '</div>';
// Frage
html += '<div style="background:rgba(255,255,255,0.05);padding:20px;border-radius:12px;margin-bottom:16px;">';
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('question') || 'Frage') + ':</div>';
html += '<div style="font-size:16px;font-weight:500;line-height:1.5;">' + item.question + '</div>';
html += '</div>';
// Eingabefeld für eigene Antwort
html += '<div id="qa-input-container" style="margin-bottom:16px;">';
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
html += '<textarea id="qa-user-answer" style="width:100%;min-height:80px;padding:12px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.05);color:var(--bp-text);font-size:14px;resize:vertical;font-family:inherit;" placeholder="' + (t('qa_type_answer') || 'Schreibe deine Antwort hier...') + '"></textarea>';
html += '</div>';
// Prüfen-Button
html += '<div id="qa-check-btn-container" style="text-align:center;margin-bottom:16px;">';
html += '<button class="btn btn-primary" id="btn-qa-check-answer" style="padding:12px 32px;">' + (t('qa_check_answer') || 'Antwort prüfen') + '</button>';
html += '</div>';
// Vergleichs-Container (versteckt)
html += '<div id="qa-comparison-container" style="display:none;">';
// Eigene Antwort (wird befüllt)
html += '<div id="qa-user-answer-display" style="background:rgba(59,130,246,0.1);padding:16px;border-radius:12px;margin-bottom:12px;border-left:3px solid #3b82f6;">';
html += '<div style="font-size:11px;color:#3b82f6;margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
html += '<div id="qa-user-answer-text" style="font-size:14px;line-height:1.5;"></div>';
html += '</div>';
// Richtige Antwort
html += '<div style="background:rgba(34,197,94,0.1);padding:16px;border-radius:12px;margin-bottom:16px;border-left:3px solid #22c55e;">';
html += '<div style="font-size:11px;color:#22c55e;margin-bottom:8px;">' + (t('qa_correct_answer') || 'Richtige Antwort') + ':</div>';
html += '<div style="font-size:14px;line-height:1.5;">' + item.answer + '</div>';
if (item.key_terms && item.key_terms.length > 0) {
html += '<div style="margin-top:12px;font-size:11px;color:var(--bp-text-muted);">' + (t('qa_key_terms') || 'Schlüsselbegriffe') + ': <span style="color:#22c55e;">' + item.key_terms.join(', ') + '</span></div>';
}
html += '</div>';
// Selbstbewertung
html += '<div style="text-align:center;margin-bottom:8px;">';
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">' + (t('qa_self_evaluate') || 'War deine Antwort richtig?') + '</div>';
html += '<div style="display:flex;gap:12px;justify-content:center;">';
html += '<button class="btn" id="btn-qa-incorrect" style="background:#ef4444;padding:12px 24px;">' + (t('qa_incorrect') || 'Falsch') + '</button>';
html += '<button class="btn" id="btn-qa-correct" style="background:#22c55e;padding:12px 24px;">' + (t('qa_correct') || 'Richtig') + '</button>';
html += '</div>';
html += '</div>';
html += '</div>'; // Ende qa-comparison-container
// Session-Statistik
html += '<div style="margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.1);display:flex;justify-content:center;gap:20px;font-size:11px;">';
html += '<div style="color:#22c55e;">' + (t('qa_session_correct') || 'Richtig') + ': ' + qaSessionStats.correct + '</div>';
html += '<div style="color:#ef4444;">' + (t('qa_session_incorrect') || 'Falsch') + ': ' + qaSessionStats.incorrect + '</div>';
html += '</div>';
qaModalBody.innerHTML = html;
// Event Listener für Prüfen-Button
document.getElementById('btn-qa-check-answer').addEventListener('click', () => {
const userAnswer = document.getElementById('qa-user-answer').value.trim();
// Zeige die eigene Antwort im Vergleich
document.getElementById('qa-user-answer-text').textContent = userAnswer || (t('qa_no_answer') || '(keine Antwort eingegeben)');
// Verstecke Eingabe, zeige Vergleich
document.getElementById('qa-input-container').style.display = 'none';
document.getElementById('qa-check-btn-container').style.display = 'none';
document.getElementById('qa-comparison-container').style.display = 'block';
});
// Enter-Taste im Textfeld löst Prüfen aus
document.getElementById('qa-user-answer').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
document.getElementById('btn-qa-check-answer').click();
}
});
// Fokus auf Textfeld setzen
setTimeout(() => {
document.getElementById('qa-user-answer').focus();
}, 100);
document.getElementById('btn-qa-correct').addEventListener('click', () => handleQaAnswer(true));
document.getElementById('btn-qa-incorrect').addEventListener('click', () => handleQaAnswer(false));
}
async function handleQaAnswer(correct) {
const item = currentQaData.qa_items[currentQaIndex];
// Update Session-Statistik
qaSessionStats.total++;
if (correct) qaSessionStats.correct++;
else qaSessionStats.incorrect++;
// Update Leitner-Fortschritt auf dem Server
try {
const currentFile = eingangFiles[currentIndex];
await fetch('/api/qa-progress?filename=' + encodeURIComponent(currentFile) + '&item_id=' + encodeURIComponent(item.id) + '&correct=' + correct, {
method: 'POST'
});
} catch (e) {
console.error('Fehler beim Speichern des Fortschritts:', e);
}
// Nächste Frage
currentQaIndex++;
renderQaLearningCard();
}
function renderQaSessionSummary() {
const percent = qaSessionStats.total > 0 ? Math.round(qaSessionStats.correct / qaSessionStats.total * 100) : 0;
let html = '';
html += '<div style="text-align:center;padding:20px;">';
html += '<div style="font-size:48px;margin-bottom:16px;">' + (percent >= 80 ? '🎉' : percent >= 50 ? '👍' : '💪') + '</div>';
html += '<div style="font-size:24px;font-weight:600;margin-bottom:8px;">' + (t('qa_session_complete') || 'Lernrunde abgeschlossen!') + '</div>';
html += '<div style="font-size:18px;margin-bottom:24px;">' + qaSessionStats.correct + ' / ' + qaSessionStats.total + ' ' + (t('qa_result_correct') || 'richtig') + ' (' + percent + '%)</div>';
// Statistik
html += '<div style="display:flex;justify-content:center;gap:24px;margin-bottom:24px;">';
html += '<div style="text-align:center;"><div style="font-size:32px;color:#22c55e;">' + qaSessionStats.correct + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_correct') || 'Richtig') + '</div></div>';
html += '<div style="text-align:center;"><div style="font-size:32px;color:#ef4444;">' + qaSessionStats.incorrect + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_incorrect') || 'Falsch') + '</div></div>';
html += '</div>';
html += '<div style="display:flex;gap:12px;justify-content:center;">';
html += '<button class="btn btn-primary" id="btn-qa-restart">' + (t('qa_restart') || 'Nochmal lernen') + '</button>';
html += '<button class="btn btn-ghost" id="btn-qa-close-summary">' + (t('close') || 'Schließen') + '</button>';
html += '</div>';
html += '</div>';
qaModalBody.innerHTML = html;
document.getElementById('btn-qa-restart').addEventListener('click', () => {
currentQaIndex = 0;
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
renderQaLearningCard();
});
document.getElementById('btn-qa-close-summary').addEventListener('click', closeQaModal);
// Aktualisiere Preview nach Session
loadQaPreviewForCurrent();
}
function openQaPrintDialog() {
if (!currentQaData) {
alert(t('qa_no_questions') || 'Keine Q&A vorhanden.');
return;
}
const currentFile = eingangFiles[currentIndex];
const baseName = currentFile.split('.')[0];
// Öffne Druck-Optionen
const choice = confirm((t('qa_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit Lösungen\\nAbbrechen = Nur Fragen');
const url = '/api/print-qa/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
window.open(url, '_blank');
}
// Event Listener für Q&A-Buttons
if (btnQaGenerate) {
btnQaGenerate.addEventListener('click', generateQaQuestions);
}
if (btnQaLearn) {
btnQaLearn.addEventListener('click', openQaModal);
}
if (btnQaPrint) {
btnQaPrint.addEventListener('click', openQaPrintDialog);
}
if (qaModalClose) {
qaModalClose.addEventListener('click', closeQaModal);
}
if (qaModal) {
qaModal.addEventListener('click', (ev) => {
if (ev.target === qaModal) {
closeQaModal();
}
});
}
// --- Sprachauswahl Event Listener ---
const languageSelect = document.getElementById('language-select');
if (languageSelect) {
// Setze initiale Auswahl basierend auf gespeicherter Sprache
languageSelect.value = currentLang;
languageSelect.addEventListener('change', (e) => {
applyLanguage(e.target.value);
});
}
// --- Initial ---
async function init() {
// Theme Toggle initialisieren
initThemeToggle();
// Sprache anwenden (aus localStorage oder default)
applyLanguage(currentLang);
updateScreen();
updateTiles();
await loadEingangFiles();
await loadWorksheetPairs();
await loadLearningUnits();
// Lade MC-Vorschau für aktuelle Datei
await loadMcPreviewForCurrent();
// Lade Lückentext-Vorschau
await loadClozePreviewForCurrent();
// Lade Q&A-Vorschau
await loadQaPreviewForCurrent();
// Lade Mindmap-Vorschau
await loadMindmapData();
// Initialisiere vast.ai Control
initVastControl();
}
// ============================================
// vast.ai GPU Control
// ============================================
const VAST_API_KEY = '88573e05868f29958022a78652e63b934812a01021e5580ed4fea35dc39b5e9c';
let vastRefreshInterval = null;
function initVastControl() {
const btnStart = document.getElementById('btn-vast-start');
const btnStop = document.getElementById('btn-vast-stop');
if (btnStart) {
btnStart.addEventListener('click', () => vastAction('start'));
}
if (btnStop) {
btnStop.addEventListener('click', () => vastAction('stop'));
}
// Initial load
refreshVastStatus();
// Auto-refresh every 30 seconds
vastRefreshInterval = setInterval(refreshVastStatus, 30000);
}
async function refreshVastStatus() {
const statusBadge = document.getElementById('vast-status-badge');
const gpuName = document.getElementById('vast-gpu-name');
const costHour = document.getElementById('vast-cost-hour');
const autoShutdown = document.getElementById('vast-auto-shutdown');
const creditEl = document.getElementById('vast-credit');
const sessionCostEl = document.getElementById('vast-session-cost');
const btnStart = document.getElementById('btn-vast-start');
const btnStop = document.getElementById('btn-vast-stop');
const msgEl = document.getElementById('vast-message');
try {
const resp = await fetch('/infra/vast/status', {
headers: { 'X-API-Key': VAST_API_KEY }
});
if (!resp.ok) {
throw new Error('API nicht erreichbar');
}
const data = await resp.json();
// Update status badge
statusBadge.textContent = data.status || 'unbekannt';
statusBadge.className = 'vast-badge vast-badge-' + (data.status || 'unknown');
// Update GPU info
gpuName.textContent = data.gpu_name || '-';
costHour.textContent = data.dph_total ? ('$' + data.dph_total.toFixed(3)) : '-';
// Update auto-shutdown
if (data.auto_shutdown_in_minutes !== null) {
autoShutdown.textContent = data.auto_shutdown_in_minutes + ' min';
} else {
autoShutdown.textContent = '-';
}
// Update Budget/Credit
if (data.account_credit !== null && data.account_credit !== undefined) {
creditEl.textContent = '$' + data.account_credit.toFixed(2);
// Color based on remaining credit
if (data.account_credit < 5) {
creditEl.style.color = 'var(--bp-danger)';
} else if (data.account_credit < 15) {
creditEl.style.color = 'var(--bp-warning)';
} else {
creditEl.style.color = 'var(--bp-success)';
}
} else {
creditEl.textContent = '-';
}
// Update Session cost
if (data.session_runtime_minutes !== null && data.session_cost_usd !== null) {
const mins = Math.round(data.session_runtime_minutes);
const cost = data.session_cost_usd.toFixed(3);
sessionCostEl.textContent = mins + ' min / $' + cost;
} else {
sessionCostEl.textContent = '-';
}
// Enable/disable buttons based on status
if (data.status === 'running') {
btnStart.disabled = true;
btnStop.disabled = false;
} else if (data.status === 'stopped' || data.status === 'exited') {
btnStart.disabled = false;
btnStop.disabled = true;
} else {
// loading, scheduling, creating
btnStart.disabled = true;
btnStop.disabled = true;
}
msgEl.textContent = '';
msgEl.className = 'vast-message';
} catch (err) {
statusBadge.textContent = 'Fehler';
statusBadge.className = 'vast-badge vast-badge-unknown';
gpuName.textContent = '-';
costHour.textContent = '-';
autoShutdown.textContent = '-';
creditEl.textContent = '-';
sessionCostEl.textContent = '-';
btnStart.disabled = true;
btnStop.disabled = true;
msgEl.textContent = err.message;
msgEl.className = 'vast-message error';
}
}
async function vastAction(action) {
const btnStart = document.getElementById('btn-vast-start');
const btnStop = document.getElementById('btn-vast-stop');
const msgEl = document.getElementById('vast-message');
const statusBadge = document.getElementById('vast-status-badge');
btnStart.disabled = true;
btnStop.disabled = true;
statusBadge.textContent = action === 'start' ? 'starting...' : 'stopping...';
statusBadge.className = 'vast-badge vast-badge-loading';
msgEl.textContent = '';
try {
const endpoint = action === 'start' ? '/infra/vast/power/on' : '/infra/vast/power/off';
const resp = await fetch(endpoint, {
method: 'POST',
headers: {
'X-API-Key': VAST_API_KEY,
'Content-Type': 'application/json'
},
body: '{}'
});
const data = await resp.json();
if (!resp.ok) {
throw new Error(data.detail || 'Aktion fehlgeschlagen');
}
msgEl.textContent = action === 'start' ? 'Start angefordert' : 'Stop angefordert';
msgEl.className = 'vast-message success';
// Refresh status after short delay
setTimeout(refreshVastStatus, 3000);
setTimeout(refreshVastStatus, 10000);
} catch (err) {
msgEl.textContent = err.message;
msgEl.className = 'vast-message error';
refreshVastStatus();
}
}
init();
</script>
<!-- Admin Panel Modal -->
<div id="admin-modal" class="admin-modal">
<div class="admin-modal-content">
<div class="admin-modal-header">
<h2><span>⚙️</span> Consent Admin Panel</h2>
<button id="admin-modal-close" class="legal-modal-close">&times;</button>
</div>
<div class="admin-tabs">
<button class="admin-tab active" data-tab="documents">Dokumente</button>
<button class="admin-tab" data-tab="versions">Versionen</button>
<button class="admin-tab" data-tab="cookies">Cookie-Kategorien</button>
<button class="admin-tab" data-tab="stats">Statistiken</button>
<button class="admin-tab" data-tab="emails">E-Mail Vorlagen</button>
<button class="admin-tab" data-tab="dsr">Betroffenenanfragen</button>
<button class="admin-tab" data-tab="dsms">DSMS</button>
</div>
<div class="admin-body">
<!-- Documents Tab -->
<div id="admin-documents" class="admin-content active">
<div class="admin-toolbar">
<div class="admin-toolbar-left">
<input type="text" class="admin-search" placeholder="Dokumente suchen..." id="admin-doc-search">
</div>
<button class="btn btn-primary btn-sm" onclick="showDocumentForm()">+ Neues Dokument</button>
</div>
<!-- Document Creation Form -->
<div id="admin-document-form" class="admin-form" style="display: none;">
<h3 class="admin-form-title" id="admin-document-form-title">Neues Dokument erstellen</h3>
<input type="hidden" id="admin-document-id">
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Dokumenttyp *</label>
<select class="admin-form-select" id="admin-document-type">
<option value="">-- Typ auswählen --</option>
<option value="terms">AGB (Allgemeine Geschäftsbedingungen)</option>
<option value="privacy">Datenschutzerklärung</option>
<option value="cookies">Cookie-Richtlinie</option>
<option value="community">Community Guidelines</option>
<option value="imprint">Impressum</option>
</select>
</div>
<div class="admin-form-group">
<label class="admin-form-label">Name *</label>
<input type="text" class="admin-form-input" id="admin-document-name" placeholder="z.B. Allgemeine Geschäftsbedingungen">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Beschreibung</label>
<input type="text" class="admin-form-input" id="admin-document-description" placeholder="Kurze Beschreibung des Dokuments">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">
<input type="checkbox" id="admin-document-mandatory" style="margin-right: 8px;">
Pflichtdokument (Nutzer müssen zustimmen)
</label>
</div>
</div>
<div class="admin-form-actions">
<button class="btn btn-ghost btn-sm" onclick="hideDocumentForm()">Abbrechen</button>
<button class="btn btn-primary btn-sm" onclick="saveDocument()">Dokument erstellen</button>
</div>
</div>
<div id="admin-doc-table-container">
<div class="admin-loading">Lade Dokumente...</div>
</div>
</div>
<!-- Versions Tab -->
<div id="admin-versions" class="admin-content">
<div class="admin-toolbar">
<div class="admin-toolbar-left">
<select class="admin-form-select" id="admin-version-doc-select" onchange="loadVersionsForDocument()">
<option value="">-- Dokument auswählen --</option>
</select>
</div>
<button class="btn btn-primary btn-sm" onclick="showVersionForm()" id="btn-new-version" disabled>+ Neue Version</button>
</div>
<div id="admin-version-form" class="admin-form">
<h3 class="admin-form-title" id="admin-version-form-title">Neue Version erstellen</h3>
<input type="hidden" id="admin-version-id">
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Version *</label>
<input type="text" class="admin-form-input" id="admin-version-number" placeholder="z.B. 1.0.0">
</div>
<div class="admin-form-group">
<label class="admin-form-label">Sprache *</label>
<select class="admin-form-select" id="admin-version-lang">
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Titel *</label>
<input type="text" class="admin-form-input" id="admin-version-title" placeholder="Titel der Version">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Zusammenfassung</label>
<input type="text" class="admin-form-input" id="admin-version-summary" placeholder="Kurze Zusammenfassung der Änderungen">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Inhalt *</label>
<div class="editor-container">
<div class="editor-toolbar">
<div class="editor-toolbar-group">
<button type="button" class="editor-btn" onclick="formatDoc('bold')" title="Fett (Strg+B)"><b>B</b></button>
<button type="button" class="editor-btn" onclick="formatDoc('italic')" title="Kursiv (Strg+I)"><i>I</i></button>
<button type="button" class="editor-btn" onclick="formatDoc('underline')" title="Unterstrichen (Strg+U)"><u>U</u></button>
</div>
<div class="editor-toolbar-group">
<button type="button" class="editor-btn" onclick="formatBlock('h1')" title="Überschrift 1">H1</button>
<button type="button" class="editor-btn" onclick="formatBlock('h2')" title="Überschrift 2">H2</button>
<button type="button" class="editor-btn" onclick="formatBlock('h3')" title="Überschrift 3">H3</button>
<button type="button" class="editor-btn" onclick="formatBlock('p')" title="Absatz">P</button>
</div>
<div class="editor-toolbar-group">
<button type="button" class="editor-btn" onclick="formatDoc('insertUnorderedList')" title="Aufzählung">• Liste</button>
<button type="button" class="editor-btn" onclick="formatDoc('insertOrderedList')" title="Nummerierung">1. Liste</button>
</div>
<div class="editor-toolbar-group">
<button type="button" class="editor-btn" onclick="insertLink()" title="Link einfügen">🔗</button>
<button type="button" class="editor-btn" onclick="formatDoc('formatBlock', 'blockquote')" title="Zitat">❝</button>
<button type="button" class="editor-btn" onclick="formatDoc('insertHorizontalRule')" title="Trennlinie">—</button>
</div>
<div class="editor-toolbar-group">
<button type="button" class="editor-btn editor-btn-upload" onclick="document.getElementById('word-upload').click()" title="Word-Dokument importieren">📄 Word Import</button>
<input type="file" id="word-upload" class="word-upload-input" accept=".docx,.doc" onchange="handleWordUpload(event)">
</div>
</div>
<div id="admin-version-editor" class="editor-content" contenteditable="true" placeholder="Schreiben Sie hier den Inhalt..."></div>
<div class="editor-status">
<span id="editor-char-count">0 Zeichen</span> |
<span style="color: var(--bp-text-muted);">Tipp: Sie können direkt aus Word kopieren und einfügen!</span>
</div>
</div>
<input type="hidden" id="admin-version-content">
</div>
</div>
<div class="admin-form-actions">
<button class="btn btn-ghost btn-sm" onclick="hideVersionForm()">Abbrechen</button>
<button class="btn btn-primary btn-sm" onclick="saveVersion()">Speichern</button>
</div>
</div>
<div id="admin-version-table-container">
<div class="admin-empty">Wählen Sie ein Dokument aus, um dessen Versionen anzuzeigen.</div>
</div>
<!-- Approval Dialog -->
<div id="approval-dialog" class="admin-dialog">
<div class="admin-dialog-content">
<h3>Version genehmigen</h3>
<p class="admin-dialog-info">
Legen Sie einen Veröffentlichungszeitpunkt fest. Die Version wird automatisch zum gewählten Zeitpunkt veröffentlicht.
</p>
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Veröffentlichungsdatum *</label>
<input type="date" class="admin-form-input" id="approval-date" required>
</div>
<div class="admin-form-group">
<label class="admin-form-label">Uhrzeit</label>
<input type="time" class="admin-form-input" id="approval-time" value="00:00">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Kommentar (optional)</label>
<input type="text" class="admin-form-input" id="approval-comment" placeholder="z.B. Genehmigt nach rechtlicher Prüfung">
</div>
</div>
<div class="admin-dialog-actions">
<button class="btn btn-ghost btn-sm" onclick="hideApprovalDialog()">Abbrechen</button>
<button class="btn btn-primary btn-sm" onclick="submitApproval()">Genehmigen & Planen</button>
</div>
</div>
</div>
</div>
<!-- Version Compare View (Full Screen Overlay) -->
<div id="version-compare-view" class="version-compare-overlay">
<div class="version-compare-header">
<h2>Versionsvergleich</h2>
<div class="version-compare-info">
<span id="compare-published-info"></span>
<span class="compare-vs">vs</span>
<span id="compare-draft-info"></span>
</div>
<button class="btn btn-ghost" onclick="hideCompareView()">Schließen</button>
</div>
<div class="version-compare-container">
<div class="version-compare-panel">
<div class="version-compare-panel-header">
<span class="compare-label compare-label-published">Veröffentlichte Version</span>
<span id="compare-published-version"></span>
</div>
<div class="version-compare-content" id="compare-content-left"></div>
</div>
<div class="version-compare-panel">
<div class="version-compare-panel-header">
<span class="compare-label compare-label-draft">Neue Version</span>
<span id="compare-draft-version"></span>
</div>
<div class="version-compare-content" id="compare-content-right"></div>
</div>
</div>
<div class="version-compare-footer">
<div id="compare-history-container"></div>
<div id="compare-actions-container" style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 12px;"></div>
</div>
</div>
<!-- Cookie Categories Tab -->
<div id="admin-cookies" class="admin-content">
<div class="admin-toolbar">
<div class="admin-toolbar-left">
<span style="color: var(--bp-text-muted);">Cookie-Kategorien verwalten</span>
</div>
<button class="btn btn-primary btn-sm" onclick="showCookieForm()">+ Neue Kategorie</button>
</div>
<div id="admin-cookie-form" class="admin-form">
<h3 class="admin-form-title">Neue Cookie-Kategorie</h3>
<input type="hidden" id="admin-cookie-id">
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Technischer Name *</label>
<input type="text" class="admin-form-input" id="admin-cookie-name" placeholder="z.B. analytics">
</div>
<div class="admin-form-group">
<label class="admin-form-label">Anzeigename (DE) *</label>
<input type="text" class="admin-form-input" id="admin-cookie-display-de" placeholder="z.B. Analyse-Cookies">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Anzeigename (EN)</label>
<input type="text" class="admin-form-input" id="admin-cookie-display-en" placeholder="z.B. Analytics Cookies">
</div>
<div class="admin-form-group">
<label class="admin-form-label">
<input type="checkbox" id="admin-cookie-mandatory"> Notwendig (kann nicht deaktiviert werden)
</label>
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Beschreibung (DE)</label>
<input type="text" class="admin-form-input" id="admin-cookie-desc-de" placeholder="Beschreibung auf Deutsch">
</div>
</div>
<div class="admin-form-actions">
<button class="btn btn-ghost btn-sm" onclick="hideCookieForm()">Abbrechen</button>
<button class="btn btn-primary btn-sm" onclick="saveCookieCategory()">Speichern</button>
</div>
</div>
<div id="admin-cookie-table-container">
<div class="admin-loading">Lade Cookie-Kategorien...</div>
</div>
</div>
<!-- Statistics Tab -->
<div id="admin-stats" class="admin-content">
<div id="admin-stats-container">
<div class="admin-loading">Lade Statistiken...</div>
</div>
</div>
<!-- E-Mail Templates Tab -->
<div id="admin-emails" class="admin-content">
<div class="admin-toolbar">
<div class="admin-toolbar-left">
<select class="admin-form-select" id="email-template-select" onchange="loadEmailTemplateVersions()">
<option value="">-- E-Mail-Vorlage auswählen --</option>
</select>
</div>
<button class="btn btn-ghost btn-sm" onclick="initializeEmailTemplates()">Templates initialisieren</button>
<button class="btn btn-primary btn-sm" onclick="showEmailVersionForm()" id="btn-new-email-version" disabled>+ Neue Version</button>
</div>
<!-- E-Mail Template Info Card -->
<div id="email-template-info" style="display: none; margin-bottom: 16px;">
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px; border: 1px solid var(--bp-border);">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<h3 style="margin: 0 0 8px 0; font-size: 16px;" id="email-template-name">-</h3>
<p style="margin: 0; color: var(--bp-text-muted); font-size: 13px;" id="email-template-description">-</p>
</div>
<div style="text-align: right;">
<div class="admin-badge" id="email-template-type-badge">-</div>
</div>
</div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--bp-border);">
<span style="font-size: 12px; color: var(--bp-text-muted);">Variablen: </span>
<span id="email-template-variables" style="font-size: 12px; font-family: monospace; color: var(--bp-primary);"></span>
</div>
</div>
</div>
<!-- E-Mail Version Form -->
<div id="email-version-form" class="admin-form" style="display: none;">
<h3 class="admin-form-title" id="email-version-form-title">Neue E-Mail-Version erstellen</h3>
<input type="hidden" id="email-version-id">
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Version *</label>
<input type="text" class="admin-form-input" id="email-version-number" placeholder="z.B. 1.0.0">
</div>
<div class="admin-form-group">
<label class="admin-form-label">Sprache *</label>
<select class="admin-form-select" id="email-version-lang">
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Betreff *</label>
<input type="text" class="admin-form-input" id="email-version-subject" placeholder="E-Mail Betreff (kann Variablen enthalten)">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">HTML-Inhalt *</label>
<div class="editor-container">
<div class="editor-toolbar">
<div class="editor-toolbar-group">
<button type="button" class="editor-btn" onclick="formatEmailDoc('bold')" title="Fett"><b>B</b></button>
<button type="button" class="editor-btn" onclick="formatEmailDoc('italic')" title="Kursiv"><i>I</i></button>
<button type="button" class="editor-btn" onclick="formatEmailDoc('underline')" title="Unterstrichen"><u>U</u></button>
</div>
<div class="editor-toolbar-group">
<button type="button" class="editor-btn" onclick="formatEmailBlock('h1')" title="Überschrift 1">H1</button>
<button type="button" class="editor-btn" onclick="formatEmailBlock('h2')" title="Überschrift 2">H2</button>
<button type="button" class="editor-btn" onclick="formatEmailBlock('p')" title="Absatz">P</button>
</div>
<div class="editor-toolbar-group">
<button type="button" class="editor-btn" onclick="insertEmailVariable()" title="Variable einfügen">{{var}}</button>
<button type="button" class="editor-btn" onclick="insertEmailLink()" title="Link einfügen">🔗</button>
<button type="button" class="editor-btn" onclick="insertEmailButton()" title="Button einfügen">🔘</button>
</div>
</div>
<div id="email-version-editor" class="editor-content" contenteditable="true" placeholder="HTML-Inhalt der E-Mail..." style="min-height: 200px;"></div>
</div>
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Text-Version (Plain Text)</label>
<textarea class="admin-form-input" id="email-version-text" rows="5" placeholder="Plain-Text-Version der E-Mail (optional, wird aus HTML generiert falls leer)"></textarea>
</div>
</div>
<div class="admin-form-actions">
<button class="btn btn-ghost btn-sm" onclick="hideEmailVersionForm()">Abbrechen</button>
<button class="btn btn-ghost btn-sm" onclick="previewEmailVersion()">Vorschau</button>
<button class="btn btn-primary btn-sm" onclick="saveEmailVersion()">Speichern</button>
</div>
</div>
<!-- E-Mail Versions Table -->
<div id="email-version-table-container">
<div class="admin-empty">Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.</div>
</div>
<!-- E-Mail Preview Dialog -->
<div id="email-preview-dialog" class="admin-dialog" style="display: none;">
<div class="admin-dialog-content" style="max-width: 700px; max-height: 80vh; overflow-y: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="margin: 0;">E-Mail Vorschau</h3>
<button class="btn btn-ghost btn-sm" onclick="hideEmailPreview()">Schließen</button>
</div>
<div style="margin-bottom: 12px;">
<strong>Betreff:</strong> <span id="email-preview-subject"></span>
</div>
<div style="border: 1px solid var(--bp-border); border-radius: 8px; padding: 16px; background: white; color: #333;">
<div id="email-preview-content"></div>
</div>
<div style="margin-top: 16px; display: flex; gap: 8px;">
<input type="email" class="admin-form-input" id="email-test-address" placeholder="Test-E-Mail-Adresse" style="flex: 1;">
<button class="btn btn-primary btn-sm" onclick="sendTestEmail()">Test-E-Mail senden</button>
</div>
</div>
</div>
<!-- E-Mail Approval Dialog -->
<div id="email-approval-dialog" class="admin-dialog" style="display: none;">
<div class="admin-dialog-content">
<h3>E-Mail-Version genehmigen</h3>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Kommentar (optional)</label>
<input type="text" class="admin-form-input" id="email-approval-comment" placeholder="z.B. Genehmigt nach Marketing-Prüfung">
</div>
</div>
<div class="admin-dialog-actions">
<button class="btn btn-ghost btn-sm" onclick="hideEmailApprovalDialog()">Abbrechen</button>
<button class="btn btn-primary btn-sm" onclick="submitEmailApproval()">Genehmigen</button>
</div>
</div>
</div>
</div>
<!-- DSR (Betroffenenanfragen) Tab -->
<div id="admin-dsr" class="admin-content">
<div class="admin-toolbar">
<div class="admin-toolbar-left">
<select class="admin-form-select" id="dsr-filter-status" onchange="loadDSRList()" style="min-width: 150px;">
<option value="">Alle Status</option>
<option value="intake">Eingang</option>
<option value="identity_verification">Identitätsprüfung</option>
<option value="processing">In Bearbeitung</option>
<option value="completed">Abgeschlossen</option>
<option value="rejected">Abgelehnt</option>
<option value="cancelled">Storniert</option>
</select>
<select class="admin-form-select" id="dsr-filter-type" onchange="loadDSRList()" style="min-width: 150px;">
<option value="">Alle Typen</option>
<option value="access">Art. 15 - Auskunft</option>
<option value="rectification">Art. 16 - Berichtigung</option>
<option value="erasure">Art. 17 - Löschung</option>
<option value="restriction">Art. 18 - Einschränkung</option>
<option value="portability">Art. 20 - Datenübertragbarkeit</option>
</select>
<label style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--bp-text-muted);">
<input type="checkbox" id="dsr-filter-overdue" onchange="loadDSRList()">
Nur überfällige
</label>
</div>
<button class="btn btn-primary btn-sm" onclick="showDSRCreateForm()">+ Neue Anfrage</button>
</div>
<!-- DSR Stats Dashboard -->
<div id="dsr-stats-cards" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin: 16px 0;">
<div class="admin-loading">Lade Statistiken...</div>
</div>
<!-- DSR Create Form -->
<div id="dsr-create-form" class="admin-form" style="display: none;">
<h3 class="admin-form-title">Neue Betroffenenanfrage erstellen</h3>
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Anfragetyp *</label>
<select class="admin-form-select" id="dsr-create-type" required>
<option value="">-- Typ wählen --</option>
<option value="access">Art. 15 - Auskunftsrecht</option>
<option value="rectification">Art. 16 - Recht auf Berichtigung</option>
<option value="erasure">Art. 17 - Recht auf Löschung</option>
<option value="restriction">Art. 18 - Recht auf Einschränkung</option>
<option value="portability">Art. 20 - Recht auf Datenübertragbarkeit</option>
</select>
</div>
<div class="admin-form-group">
<label class="admin-form-label">Priorität</label>
<select class="admin-form-select" id="dsr-create-priority">
<option value="normal">Normal</option>
<option value="high">Hoch</option>
<option value="expedited">Beschleunigt</option>
</select>
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">E-Mail des Antragstellers *</label>
<input type="email" class="admin-form-input" id="dsr-create-email" required placeholder="email@example.com">
</div>
<div class="admin-form-group">
<label class="admin-form-label">Name des Antragstellers</label>
<input type="text" class="admin-form-input" id="dsr-create-name" placeholder="Max Mustermann">
</div>
</div>
<div class="admin-form-row">
<div class="admin-form-group full-width">
<label class="admin-form-label">Telefon</label>
<input type="text" class="admin-form-input" id="dsr-create-phone" placeholder="+49 123 456789">
</div>
</div>
<div class="admin-form-actions">
<button class="btn btn-ghost btn-sm" onclick="hideDSRCreateForm()">Abbrechen</button>
<button class="btn btn-primary btn-sm" onclick="createDSR()">Anfrage erstellen</button>
</div>
</div>
<!-- DSR Table -->
<div id="dsr-table-container">
<div class="admin-loading">Lade Betroffenenanfragen...</div>
</div>
<!-- DSR Detail View -->
<div id="dsr-detail-view" style="display: none;">
<div class="admin-toolbar" style="margin-bottom: 16px;">
<button class="btn btn-ghost btn-sm" onclick="hideDSRDetail()">← Zurück zur Liste</button>
<div style="display: flex; gap: 8px;">
<button class="btn btn-ghost btn-sm" id="dsr-btn-verify" onclick="verifyDSRIdentity()">Identität verifizieren</button>
<button class="btn btn-ghost btn-sm" id="dsr-btn-assign" onclick="showDSRAssignDialog()">Zuweisen</button>
<button class="btn btn-ghost btn-sm" id="dsr-btn-extend" onclick="showDSRExtendDialog()">Frist verlängern</button>
<button class="btn btn-primary btn-sm" id="dsr-btn-complete" onclick="showDSRCompleteDialog()">Abschließen</button>
<button class="btn btn-danger btn-sm" id="dsr-btn-reject" onclick="showDSRRejectDialog()">Ablehnen</button>
</div>
</div>
<div id="dsr-detail-content"></div>
</div>
</div>
<!-- DSMS Tab -->
<div id="admin-dsms" class="admin-content">
<div class="admin-toolbar">
<div class="admin-toolbar-left">
<span style="font-weight: 600; color: var(--bp-primary);">Dezentrales Speichersystem (IPFS)</span>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-ghost btn-sm" onclick="openDsmsWebUI()">DSMS WebUI</button>
<button class="btn btn-primary btn-sm" onclick="loadDsmsData()">Aktualisieren</button>
</div>
</div>
<!-- DSMS Status Cards -->
<div id="dsms-status-cards" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px;">
<div class="admin-loading">Lade DSMS Status...</div>
</div>
<!-- DSMS Tabs -->
<div style="display: flex; gap: 8px; margin-bottom: 16px; border-bottom: 1px solid var(--bp-border); padding-bottom: 8px;">
<button class="dsms-subtab active" data-dsms-tab="archives" onclick="switchDsmsTab('archives')">Archivierte Dokumente</button>
<button class="dsms-subtab" data-dsms-tab="verify" onclick="switchDsmsTab('verify')">Verifizierung</button>
<button class="dsms-subtab" data-dsms-tab="settings" onclick="switchDsmsTab('settings')">Einstellungen</button>
</div>
<!-- Archives Sub-Tab -->
<div id="dsms-archives" class="dsms-content active">
<div class="admin-toolbar" style="margin-bottom: 16px;">
<div class="admin-toolbar-left">
<input type="text" class="admin-search" placeholder="CID suchen..." id="dsms-cid-search" style="width: 300px;">
</div>
<button class="btn btn-primary btn-sm" onclick="showArchiveForm()">+ Dokument archivieren</button>
</div>
<!-- Archive Form -->
<div id="dsms-archive-form" class="admin-form" style="display: none; margin-bottom: 16px;">
<h3 class="admin-form-title">Dokument im DSMS archivieren</h3>
<div class="admin-form-row">
<div class="admin-form-group">
<label class="admin-form-label">Dokument auswählen *</label>
<select class="admin-form-select" id="dsms-archive-doc-select">
<option value="">-- Dokument wählen --</option>
</select>
</div>
<div class="admin-form-group">
<label class="admin-form-label">Version *</label>
<select class="admin-form-select" id="dsms-archive-version-select" disabled>
<option value="">-- Erst Dokument wählen --</option>
</select>
</div>
</div>
<div class="admin-form-actions">
<button class="btn btn-ghost btn-sm" onclick="hideArchiveForm()">Abbrechen</button>
<button class="btn btn-primary btn-sm" onclick="archiveDocumentToDsms()">Archivieren</button>
</div>
</div>
<div id="dsms-archives-table">
<div class="admin-loading">Lade archivierte Dokumente...</div>
</div>
</div>
<!-- Verify Sub-Tab -->
<div id="dsms-verify" class="dsms-content" style="display: none;">
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Dokumentenintegrität prüfen</h3>
<p style="color: var(--bp-text-muted); margin-bottom: 16px; font-size: 14px;">
Geben Sie einen CID (Content Identifier) ein, um die Integrität eines archivierten Dokuments zu verifizieren.
</p>
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<input type="text" class="admin-form-input" id="dsms-verify-cid" placeholder="Qm... oder bafy..." style="flex: 1;">
<button class="btn btn-primary btn-sm" onclick="verifyDsmsDocument()">Verifizieren</button>
</div>
<div id="dsms-verify-result" style="display: none;"></div>
</div>
</div>
<!-- Settings Sub-Tab -->
<div id="dsms-settings" class="dsms-content" style="display: none;">
<div style="display: grid; gap: 16px;">
<!-- Node Info -->
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Node-Informationen</h3>
<div id="dsms-node-info">
<div class="admin-loading">Lade Node-Info...</div>
</div>
</div>
<!-- Quick Links -->
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Schnellzugriff</h3>
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
<button class="btn btn-primary btn-sm" onclick="openDsmsWebUI()">DSMS WebUI</button>
<a href="http://localhost:8082/docs" target="_blank" class="btn btn-ghost btn-sm">DSMS API Docs</a>
<a href="http://localhost:8085" target="_blank" class="btn btn-ghost btn-sm">IPFS Gateway</a>
</div>
</div>
<!-- Lizenzhinweise -->
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Open Source Lizenzen</h3>
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 12px;">
DSMS verwendet folgende Open-Source-Komponenten:
</p>
<ul style="color: var(--bp-text-muted); font-size: 13px; margin: 0; padding-left: 20px;">
<li><strong>IPFS Kubo</strong> - MIT + Apache 2.0 (Dual License) - Protocol Labs, Inc.</li>
<li><strong>IPFS WebUI</strong> - MIT License - Protocol Labs, Inc.</li>
<li><strong>FastAPI</strong> - MIT License</li>
</ul>
<p style="color: var(--bp-text-muted); font-size: 12px; margin-top: 12px; font-style: italic;">
Alle Komponenten erlauben kommerzielle Nutzung.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- DSMS WebUI Modal -->
<div id="dsms-webui-modal" class="legal-modal" style="display: none;">
<div class="legal-modal-content" style="max-width: 1200px; width: 95%; height: 90vh;">
<div class="legal-modal-header" style="border-bottom: 1px solid var(--bp-border);">
<h2 style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 24px;">&#127760;</span> DSMS WebUI
</h2>
<button id="dsms-webui-modal-close" class="legal-modal-close" onclick="closeDsmsWebUI()">&times;</button>
</div>
<div style="display: flex; height: calc(100% - 60px);">
<!-- Sidebar -->
<div style="width: 200px; background: var(--bp-surface); border-right: 1px solid var(--bp-border); padding: 16px;">
<nav style="display: flex; flex-direction: column; gap: 4px;">
<button class="dsms-webui-nav active" data-section="overview" onclick="switchDsmsWebUISection('overview')">
<span>&#128200;</span> Übersicht
</button>
<button class="dsms-webui-nav" data-section="files" onclick="switchDsmsWebUISection('files')">
<span>&#128193;</span> Dateien
</button>
<button class="dsms-webui-nav" data-section="explore" onclick="switchDsmsWebUISection('explore')">
<span>&#128269;</span> Erkunden
</button>
<button class="dsms-webui-nav" data-section="peers" onclick="switchDsmsWebUISection('peers')">
<span>&#127760;</span> Peers
</button>
<button class="dsms-webui-nav" data-section="config" onclick="switchDsmsWebUISection('config')">
<span>&#9881;</span> Konfiguration
</button>
</nav>
</div>
<!-- Main Content -->
<div style="flex: 1; overflow-y: auto; padding: 24px;" id="dsms-webui-content">
<!-- Overview Section (default) -->
<div id="dsms-webui-overview" class="dsms-webui-section active">
<h3 style="margin: 0 0 24px 0;">Node Übersicht</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px;">
<div class="dsms-webui-stat-card">
<div class="dsms-webui-stat-label">Status</div>
<div class="dsms-webui-stat-value" id="webui-status">--</div>
</div>
<div class="dsms-webui-stat-card">
<div class="dsms-webui-stat-label">Node ID</div>
<div class="dsms-webui-stat-value" id="webui-node-id" style="font-size: 11px; word-break: break-all;">--</div>
</div>
<div class="dsms-webui-stat-card">
<div class="dsms-webui-stat-label">Protokoll</div>
<div class="dsms-webui-stat-value" id="webui-protocol">--</div>
</div>
<div class="dsms-webui-stat-card">
<div class="dsms-webui-stat-label">Agent</div>
<div class="dsms-webui-stat-value" id="webui-agent">--</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px;">
<div class="dsms-webui-stat-card">
<div class="dsms-webui-stat-label">Repo Größe</div>
<div class="dsms-webui-stat-value" id="webui-repo-size">--</div>
<div class="dsms-webui-stat-sub" id="webui-storage-info">--</div>
</div>
<div class="dsms-webui-stat-card">
<div class="dsms-webui-stat-label">Objekte</div>
<div class="dsms-webui-stat-value" id="webui-num-objects">--</div>
</div>
<div class="dsms-webui-stat-card">
<div class="dsms-webui-stat-label">Gepinnte Dokumente</div>
<div class="dsms-webui-stat-value" id="webui-pinned-count">--</div>
</div>
</div>
<div style="margin-top: 24px;">
<h4 style="margin: 0 0 12px 0;">Adressen</h4>
<div id="webui-addresses" style="background: var(--bp-input-bg); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto;">
Lade...
</div>
</div>
</div>
<!-- Files Section -->
<div id="dsms-webui-files" class="dsms-webui-section" style="display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h3 style="margin: 0;">Dateien hochladen</h3>
</div>
<div class="dsms-webui-upload-zone" id="dsms-upload-zone" ondrop="handleDsmsFileDrop(event)" ondragover="handleDsmsDragOver(event)" ondragleave="handleDsmsDragLeave(event)">
<div style="text-align: center;">
<div style="font-size: 48px; margin-bottom: 16px;">&#128229;</div>
<p style="color: var(--bp-text); margin-bottom: 8px;">Dateien hierher ziehen</p>
<p style="color: var(--bp-text-muted); font-size: 13px;">oder</p>
<input type="file" id="dsms-file-input" style="display: none;" onchange="handleDsmsFileSelect(event)" multiple>
<button class="btn btn-primary btn-sm" onclick="document.getElementById('dsms-file-input').click()">Dateien auswählen</button>
</div>
</div>
<div id="dsms-upload-progress" style="display: none; margin-top: 16px;">
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
<div id="dsms-upload-status">Hochladen...</div>
<div style="background: var(--bp-border); border-radius: 4px; height: 8px; margin-top: 8px; overflow: hidden;">
<div id="dsms-upload-bar" style="background: var(--bp-primary); height: 100%; width: 0%; transition: width 0.3s;"></div>
</div>
</div>
</div>
<div id="dsms-upload-results" style="margin-top: 24px;"></div>
</div>
<!-- Explore Section -->
<div id="dsms-webui-explore" class="dsms-webui-section" style="display: none;">
<h3 style="margin: 0 0 24px 0;">IPFS Explorer</h3>
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<input type="text" class="admin-search" placeholder="CID eingeben (z.B. QmXyz...)" id="webui-explore-cid" style="flex: 1;">
<button class="btn btn-primary btn-sm" onclick="exploreDsmsCid()">Erkunden</button>
</div>
<div id="dsms-explore-result" style="display: none;">
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
<div id="dsms-explore-content"></div>
</div>
</div>
</div>
<!-- Peers Section -->
<div id="dsms-webui-peers" class="dsms-webui-section" style="display: none;">
<h3 style="margin: 0 0 24px 0;">Verbundene Peers</h3>
<p style="color: var(--bp-text-muted); margin-bottom: 16px;">
Hinweis: In einem privaten DSMS-Netzwerk sind normalerweise keine externen Peers verbunden.
</p>
<div id="webui-peers-list">
<div class="admin-loading">Lade Peers...</div>
</div>
</div>
<!-- Config Section -->
<div id="dsms-webui-config" class="dsms-webui-section" style="display: none;">
<h3 style="margin: 0 0 24px 0;">Konfiguration</h3>
<div style="display: grid; gap: 16px;">
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
<h4 style="margin: 0 0 12px 0;">API Endpoints</h4>
<table style="width: 100%; font-size: 13px;">
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">IPFS API</td>
<td style="padding: 8px 0; font-family: monospace;">http://localhost:5001</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">DSMS Gateway</td>
<td style="padding: 8px 0; font-family: monospace;">http://localhost:8082</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">IPFS Gateway</td>
<td style="padding: 8px 0; font-family: monospace;">http://localhost:8085</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">Swarm P2P</td>
<td style="padding: 8px 0; font-family: monospace;">:4001</td>
</tr>
</table>
</div>
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
<h4 style="margin: 0 0 12px 0;">Aktionen</h4>
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
<button class="btn btn-ghost btn-sm" onclick="runDsmsGarbageCollection()">&#128465; Garbage Collection</button>
<button class="btn btn-ghost btn-sm" onclick="loadDsmsWebUIData()">&#8635; Daten aktualisieren</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Legal Modal -->
<div id="legal-modal" class="legal-modal">
<div class="legal-modal-content">
<div class="legal-modal-header">
<h2>Rechtliches</h2>
<button id="legal-modal-close" class="legal-modal-close">&times;</button>
</div>
<div class="legal-tabs">
<button class="legal-tab active" data-tab="terms">AGB</button>
<button class="legal-tab" data-tab="privacy">Datenschutz</button>
<button class="legal-tab" data-tab="community">Community Guidelines</button>
<button class="legal-tab" data-tab="cookies">Cookie-Einstellungen</button>
<button class="legal-tab" data-tab="gdpr">DSGVO-Rechte</button>
</div>
<div class="legal-body">
<div id="legal-terms" class="legal-content active">
<div id="legal-terms-content">
<div class="legal-loading">Lade AGB...</div>
</div>
</div>
<div id="legal-privacy" class="legal-content">
<div id="legal-privacy-content">
<div class="legal-loading">Lade Datenschutzerklärung...</div>
</div>
</div>
<div id="legal-community" class="legal-content">
<div id="legal-community-content">
<div class="legal-loading">Lade Community Guidelines...</div>
</div>
</div>
<div id="legal-cookies" class="legal-content">
<h3>Cookie-Einstellungen</h3>
<p>Wir verwenden Cookies, um Ihnen die bestmögliche Erfahrung zu bieten. Hier können Sie Ihre Präferenzen jederzeit anpassen.</p>
<div id="cookie-categories-container" class="cookie-categories">
<div class="legal-loading">Lade Cookie-Kategorien...</div>
</div>
<button class="btn btn-primary" style="margin-top:16px" onclick="saveCookiePreferences()">Einstellungen speichern</button>
</div>
<div id="legal-gdpr" class="legal-content">
<h3>Ihre DSGVO-Rechte</h3>
<p>Nach der Datenschutz-Grundverordnung haben Sie folgende Rechte:</p>
<div class="gdpr-actions">
<div class="gdpr-action">
<h4>📋 Datenauskunft (Art. 15)</h4>
<p>Erfahren Sie, welche Daten wir über Sie gespeichert haben.</p>
<button class="btn btn-sm" onclick="requestDataExport()">Meine Daten anfordern</button>
</div>
<div class="gdpr-action">
<h4>📤 Datenübertragbarkeit (Art. 20)</h4>
<p>Exportieren Sie Ihre Daten in einem maschinenlesbaren Format.</p>
<button class="btn btn-sm" onclick="requestDataDownload()">Daten exportieren</button>
</div>
<div class="gdpr-action">
<h4>🗑️ Recht auf Löschung (Art. 17)</h4>
<p>Beantragen Sie die vollständige Löschung Ihrer Daten.</p>
<button class="btn btn-sm btn-danger" onclick="requestDataDeletion()">Löschung beantragen</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Imprint Modal (Impressum - muss direkt erreichbar sein) -->
<div id="imprint-modal" class="legal-modal">
<div class="legal-modal-content">
<div class="legal-modal-header">
<h2>Impressum</h2>
<button id="imprint-modal-close" class="legal-modal-close">&times;</button>
</div>
<div class="legal-body" style="padding: 24px;">
<div id="imprint-content">
<div class="legal-loading">Lade Impressum...</div>
</div>
</div>
</div>
</div>
<!-- Auth Modal -->
<div id="auth-modal" class="auth-modal">
<div class="auth-modal-content">
<div class="auth-modal-header">
<h2><span>🔐</span> Anmeldung</h2>
<button id="auth-modal-close" class="legal-modal-close">&times;</button>
</div>
<div class="auth-tabs">
<button class="auth-tab active" data-tab="login">Anmelden</button>
<button class="auth-tab" data-tab="register">Registrieren</button>
</div>
<div class="auth-body">
<!-- Login Tab -->
<div id="auth-login" class="auth-content active">
<div id="auth-login-error" class="auth-error"></div>
<div id="auth-login-success" class="auth-success"></div>
<form id="auth-login-form">
<div class="auth-form-group">
<label class="auth-form-label">E-Mail</label>
<input type="email" class="auth-form-input" id="login-email" placeholder="ihre@email.de" required>
</div>
<div class="auth-form-group">
<label class="auth-form-label">Passwort</label>
<input type="password" class="auth-form-input" id="login-password" placeholder="Ihr Passwort" required>
</div>
<button type="submit" class="auth-btn auth-btn-primary" id="login-btn">Anmelden</button>
</form>
<div class="auth-link">
<a href="#" id="auth-forgot-password">Passwort vergessen?</a>
</div>
</div>
<!-- Register Tab -->
<div id="auth-register" class="auth-content">
<div id="auth-register-error" class="auth-error"></div>
<div id="auth-register-success" class="auth-success"></div>
<form id="auth-register-form">
<div class="auth-form-group">
<label class="auth-form-label">Name (optional)</label>
<input type="text" class="auth-form-input" id="register-name" placeholder="Ihr Name">
</div>
<div class="auth-form-group">
<label class="auth-form-label">E-Mail</label>
<input type="email" class="auth-form-input" id="register-email" placeholder="ihre@email.de" required>
</div>
<div class="auth-form-group">
<label class="auth-form-label">Passwort</label>
<input type="password" class="auth-form-input" id="register-password" placeholder="Mind. 8 Zeichen" required minlength="8">
</div>
<div class="auth-form-group">
<label class="auth-form-label">Passwort bestätigen</label>
<input type="password" class="auth-form-input" id="register-password-confirm" placeholder="Passwort wiederholen" required>
</div>
<button type="submit" class="auth-btn auth-btn-primary" id="register-btn">Registrieren</button>
</form>
<div class="auth-link">
Bereits registriert? <a href="#" id="auth-goto-login">Hier anmelden</a>
</div>
</div>
<!-- Forgot Password Tab (hidden by default) -->
<div id="auth-forgot" class="auth-content">
<div id="auth-forgot-error" class="auth-error"></div>
<div id="auth-forgot-success" class="auth-success"></div>
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 16px;">
Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.
</p>
<form id="auth-forgot-form">
<div class="auth-form-group">
<label class="auth-form-label">E-Mail</label>
<input type="email" class="auth-form-input" id="forgot-email" placeholder="ihre@email.de" required>
</div>
<button type="submit" class="auth-btn auth-btn-primary" id="forgot-btn">Link senden</button>
</form>
<div class="auth-link">
<a href="#" id="auth-back-to-login">Zurück zur Anmeldung</a>
</div>
</div>
<!-- Reset Password Tab (hidden by default, shown via URL token) -->
<div id="auth-reset" class="auth-content">
<div id="auth-reset-error" class="auth-error"></div>
<div id="auth-reset-success" class="auth-success"></div>
<form id="auth-reset-form">
<div class="auth-form-group">
<label class="auth-form-label">Neues Passwort</label>
<input type="password" class="auth-form-input" id="reset-password" placeholder="Mind. 8 Zeichen" required minlength="8">
</div>
<div class="auth-form-group">
<label class="auth-form-label">Passwort bestätigen</label>
<input type="password" class="auth-form-input" id="reset-password-confirm" placeholder="Passwort wiederholen" required>
</div>
<input type="hidden" id="reset-token">
<button type="submit" class="auth-btn auth-btn-primary" id="reset-btn">Passwort ändern</button>
</form>
</div>
<!-- Email Verification Tab (hidden by default, shown via URL token) -->
<div id="auth-verify" class="auth-content">
<div id="auth-verify-error" class="auth-error"></div>
<div id="auth-verify-success" class="auth-success"></div>
<div style="text-align: center; padding: 20px 0;">
<div id="auth-verify-loading" style="color: var(--bp-text-muted);">
E-Mail wird verifiziert...
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Notification Preferences Modal -->
<div id="notification-prefs-modal" class="notification-prefs-modal">
<div class="notification-prefs-content">
<div class="auth-modal-header">
<h2>Benachrichtigungseinstellungen</h2>
<button class="legal-modal-close" onclick="closeNotificationPreferences()">&times;</button>
</div>
<div class="auth-body">
<div class="notification-pref-item">
<div>
<div class="notification-pref-label">E-Mail-Benachrichtigungen</div>
<div class="notification-pref-desc">Wichtige Updates per E-Mail erhalten</div>
</div>
<div class="toggle-switch active" id="pref-email-toggle" onclick="toggleNotificationPref('email')">
<div class="toggle-switch-handle"></div>
</div>
</div>
<div class="notification-pref-item">
<div>
<div class="notification-pref-label">In-App-Benachrichtigungen</div>
<div class="notification-pref-desc">Benachrichtigungen in der App anzeigen</div>
</div>
<div class="toggle-switch active" id="pref-inapp-toggle" onclick="toggleNotificationPref('inapp')">
<div class="toggle-switch-handle"></div>
</div>
</div>
<div class="notification-pref-item">
<div>
<div class="notification-pref-label">Push-Benachrichtigungen</div>
<div class="notification-pref-desc">Browser-Push-Benachrichtigungen aktivieren</div>
</div>
<div class="toggle-switch" id="pref-push-toggle" onclick="toggleNotificationPref('push')">
<div class="toggle-switch-handle"></div>
</div>
</div>
<div style="margin-top: 20px;">
<button class="auth-btn auth-btn-primary" onclick="saveNotificationPreferences()">Speichern</button>
</div>
</div>
</div>
</div>
<!-- Suspension Overlay -->
<div id="suspension-overlay" class="suspension-overlay">
<div class="suspension-content">
<div class="suspension-icon">🚫</div>
<div class="suspension-title">Account vorübergehend gesperrt</div>
<div class="suspension-message">
Ihr Account wurde gesperrt, da ausstehende Zustimmungen nicht innerhalb der Frist erteilt wurden.
Bitte bestätigen Sie die folgenden Dokumente, um Ihren Account wiederherzustellen.
</div>
<div class="suspension-docs">
<div class="suspension-docs-title">Ausstehende Dokumente:</div>
<div id="suspension-doc-list">
<!-- Will be populated dynamically -->
</div>
</div>
<button class="suspension-btn" onclick="showConsentModal()">
Jetzt bestätigen
</button>
<div class="suspension-countdown" id="suspension-countdown"></div>
</div>
</div>
<script>
// GDPR Actions
async function saveCookiePreferences() {
const functional = document.getElementById('cookie-functional')?.checked || false;
const analytics = document.getElementById('cookie-analytics')?.checked || false;
// Save to localStorage for now
localStorage.setItem('bp_cookies', JSON.stringify({functional, analytics}));
alert('Cookie-Einstellungen gespeichert!');
}
async function requestDataExport() {
alert('Ihre Datenanfrage wurde erstellt. Sie erhalten eine E-Mail, sobald Ihre Daten bereit sind.');
}
async function requestDataDownload() {
alert('Ihr Datenexport wurde gestartet. Sie erhalten eine E-Mail mit dem Download-Link.');
}
async function requestDataDeletion() {
if (confirm('Sind Sie sicher, dass Sie alle Ihre Daten löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) {
alert('Ihre Löschanfrage wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
}
}
// Load saved cookie preferences
const savedCookies = localStorage.getItem('bp_cookies');
if (savedCookies) {
const prefs = JSON.parse(savedCookies);
if (document.getElementById('cookie-functional')) {
document.getElementById('cookie-functional').checked = prefs.functional;
}
if (document.getElementById('cookie-analytics')) {
document.getElementById('cookie-analytics').checked = prefs.analytics;
}
}
// ==========================================
// LEGAL MODAL (now after modal HTML exists)
// ==========================================
const legalModal = document.getElementById('legal-modal');
const legalModalClose = document.getElementById('legal-modal-close');
const legalTabs = document.querySelectorAll('.legal-tab');
const legalContents = document.querySelectorAll('.legal-content');
const btnLegal = document.getElementById('btn-legal');
// Imprint Modal
const imprintModal = document.getElementById('imprint-modal');
const imprintModalClose = document.getElementById('imprint-modal-close');
// Open legal modal from footer
function openLegalModal(tab = 'terms') {
legalModal.classList.add('active');
// Switch to specified tab
if (tab) {
legalTabs.forEach(t => t.classList.remove('active'));
legalContents.forEach(c => c.classList.remove('active'));
const targetTab = document.querySelector(`.legal-tab[data-tab="${tab}"]`);
if (targetTab) targetTab.classList.add('active');
document.getElementById(`legal-${tab}`)?.classList.add('active');
}
loadLegalDocuments();
}
// Open imprint modal from footer
function openImprintModal() {
imprintModal.classList.add('active');
loadImprintContent();
}
// Open legal modal
btnLegal?.addEventListener('click', async () => {
openLegalModal();
});
// Close legal modal
legalModalClose?.addEventListener('click', () => {
legalModal.classList.remove('active');
});
// Close imprint modal
imprintModalClose?.addEventListener('click', () => {
imprintModal.classList.remove('active');
});
// Close on background click
legalModal?.addEventListener('click', (e) => {
if (e.target === legalModal) {
legalModal.classList.remove('active');
}
});
imprintModal?.addEventListener('click', (e) => {
if (e.target === imprintModal) {
imprintModal.classList.remove('active');
}
});
// Tab switching
legalTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
legalTabs.forEach(t => t.classList.remove('active'));
legalContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`legal-${tabId}`)?.classList.add('active');
// Load cookie categories when switching to cookies tab
if (tabId === 'cookies') {
loadCookieCategories();
}
});
});
// Load legal documents from consent service
async function loadLegalDocuments() {
const lang = document.getElementById('language-select')?.value || 'de';
// Load all documents in parallel
await Promise.all([
loadDocumentContent('terms', 'legal-terms-content', getDefaultTerms, lang),
loadDocumentContent('privacy', 'legal-privacy-content', getDefaultPrivacy, lang),
loadDocumentContent('community_guidelines', 'legal-community-content', getDefaultCommunityGuidelines, lang)
]);
}
// Load imprint content
async function loadImprintContent() {
const lang = document.getElementById('language-select')?.value || 'de';
await loadDocumentContent('imprint', 'imprint-content', getDefaultImprint, lang);
}
// Generic function to load document content
async function loadDocumentContent(docType, containerId, defaultFn, lang) {
const container = document.getElementById(containerId);
if (!container) return;
try {
const res = await fetch(`/api/consent/documents/${docType}/latest?language=${lang}`);
if (res.ok) {
const data = await res.json();
if (data.content) {
container.innerHTML = data.content;
return;
}
}
} catch(e) {
console.log(`Could not load ${docType}:`, e);
}
// Fallback to default
container.innerHTML = defaultFn(lang);
}
// Load cookie categories for the cookie settings tab
async function loadCookieCategories() {
const container = document.getElementById('cookie-categories-container');
if (!container) return;
try {
const res = await fetch('/api/consent/cookies/categories');
if (res.ok) {
const data = await res.json();
const categories = data.categories || [];
if (categories.length === 0) {
container.innerHTML = getDefaultCookieCategories();
return;
}
// Get current preferences from localStorage
const savedPrefs = JSON.parse(localStorage.getItem('bp_cookie_consent') || '{}');
container.innerHTML = categories.map(cat => `
<label class="cookie-category">
<input type="checkbox" id="cookie-${cat.name}"
${cat.is_mandatory ? 'checked disabled' : (savedPrefs[cat.name] ? 'checked' : '')}>
<span>
<strong>${cat.display_name_de || cat.name}</strong>
${cat.is_mandatory ? ' (erforderlich)' : ''}
${cat.description_de ? ` - ${cat.description_de}` : ''}
</span>
</label>
`).join('');
} else {
container.innerHTML = getDefaultCookieCategories();
}
} catch(e) {
container.innerHTML = getDefaultCookieCategories();
}
}
function getDefaultCookieCategories() {
return `
<label class="cookie-category">
<input type="checkbox" checked disabled>
<span><strong>Notwendig</strong> (erforderlich) - Erforderlich für die Grundfunktionen</span>
</label>
<label class="cookie-category">
<input type="checkbox" id="cookie-functional">
<span><strong>Funktional</strong> - Erweiterte Funktionen und Personalisierung</span>
</label>
<label class="cookie-category">
<input type="checkbox" id="cookie-analytics">
<span><strong>Analyse</strong> - Hilft uns, die Nutzung zu verstehen</span>
</label>
`;
}
function getDefaultTerms(lang) {
const terms = {
de: '<h3>Allgemeine Geschäftsbedingungen</h3><p>Die BreakPilot-Plattform wird von der BreakPilot UG bereitgestellt.</p><p><strong>Nutzung:</strong> Die Plattform dient zur Erstellung und Verwaltung von Lernmaterialien für Bildungszwecke.</p><p><strong>Haftung:</strong> Die Nutzung erfolgt auf eigene Verantwortung.</p><p><strong>Änderungen:</strong> Wir behalten uns vor, diese AGB jederzeit zu ändern.</p>',
en: '<h3>Terms of Service</h3><p>The BreakPilot platform is provided by BreakPilot UG.</p><p><strong>Usage:</strong> The platform is designed for creating and managing learning materials for educational purposes.</p><p><strong>Liability:</strong> Use at your own risk.</p><p><strong>Changes:</strong> We reserve the right to modify these terms at any time.</p>'
};
return terms[lang] || terms.de;
}
function getDefaultPrivacy(lang) {
const privacy = {
de: '<h3>Datenschutzerklärung</h3><p><strong>Verantwortlicher:</strong> BreakPilot UG</p><p><strong>Erhobene Daten:</strong> Bei der Nutzung werden technische Daten (IP-Adresse, Browser-Typ) sowie von Ihnen eingegebene Inhalte verarbeitet.</p><p><strong>Zweck:</strong> Die Daten werden zur Bereitstellung der Plattform und zur Verbesserung unserer Dienste genutzt.</p><p><strong>Ihre Rechte (DSGVO):</strong></p><ul><li>Auskunftsrecht (Art. 15)</li><li>Recht auf Berichtigung (Art. 16)</li><li>Recht auf Löschung (Art. 17)</li><li>Recht auf Datenübertragbarkeit (Art. 20)</li></ul><p><strong>Kontakt:</strong> datenschutz@breakpilot.app</p>',
en: '<h3>Privacy Policy</h3><p><strong>Controller:</strong> BreakPilot UG</p><p><strong>Data Collected:</strong> Technical data (IP address, browser type) and content you provide are processed.</p><p><strong>Purpose:</strong> Data is used to provide the platform and improve our services.</p><p><strong>Your Rights (GDPR):</strong></p><ul><li>Right of access (Art. 15)</li><li>Right to rectification (Art. 16)</li><li>Right to erasure (Art. 17)</li><li>Right to data portability (Art. 20)</li></ul><p><strong>Contact:</strong> privacy@breakpilot.app</p>'
};
return privacy[lang] || privacy.de;
}
function getDefaultCommunityGuidelines(lang) {
const guidelines = {
de: '<h3>Community Guidelines</h3><p>Willkommen bei BreakPilot! Um eine positive und respektvolle Umgebung zu gewährleisten, bitten wir alle Nutzer, diese Richtlinien zu befolgen.</p><p><strong>Respektvoller Umgang:</strong> Behandeln Sie andere Nutzer mit Respekt und Höflichkeit.</p><p><strong>Keine illegalen Inhalte:</strong> Das Erstellen oder Teilen von illegalen Inhalten ist streng untersagt.</p><p><strong>Urheberrecht:</strong> Respektieren Sie das geistige Eigentum anderer. Verwenden Sie nur Inhalte, für die Sie die Rechte besitzen.</p><p><strong>Datenschutz:</strong> Teilen Sie keine persönlichen Daten anderer ohne deren ausdrückliche Zustimmung.</p><p><strong>Qualität:</strong> Bemühen Sie sich um qualitativ hochwertige Lerninhalte.</p><p>Verstöße gegen diese Richtlinien können zur Sperrung des Accounts führen.</p>',
en: '<h3>Community Guidelines</h3><p>Welcome to BreakPilot! To ensure a positive and respectful environment, we ask all users to follow these guidelines.</p><p><strong>Respectful Behavior:</strong> Treat other users with respect and courtesy.</p><p><strong>No Illegal Content:</strong> Creating or sharing illegal content is strictly prohibited.</p><p><strong>Copyright:</strong> Respect the intellectual property of others. Only use content you have rights to.</p><p><strong>Privacy:</strong> Do not share personal data of others without their explicit consent.</p><p><strong>Quality:</strong> Strive for high-quality learning content.</p><p>Violations of these guidelines may result in account suspension.</p>'
};
return guidelines[lang] || guidelines.de;
}
function getDefaultImprint(lang) {
const imprint = {
de: '<h3>Impressum</h3><p><strong>Angaben gemäß § 5 TMG:</strong></p><p>BreakPilot UG (haftungsbeschränkt)<br>Musterstraße 1<br>12345 Musterstadt<br>Deutschland</p><p><strong>Vertreten durch:</strong><br>Geschäftsführer: Max Mustermann</p><p><strong>Kontakt:</strong><br>Telefon: +49 (0) 123 456789<br>E-Mail: info@breakpilot.app</p><p><strong>Registereintrag:</strong><br>Eintragung im Handelsregister<br>Registergericht: Amtsgericht Musterstadt<br>Registernummer: HRB 12345</p><p><strong>Umsatzsteuer-ID:</strong><br>Umsatzsteuer-Identifikationsnummer gemäß § 27 a UStG: DE123456789</p><p><strong>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>',
en: '<h3>Legal Notice</h3><p><strong>Information according to § 5 TMG:</strong></p><p>BreakPilot UG (limited liability)<br>Musterstraße 1<br>12345 Musterstadt<br>Germany</p><p><strong>Represented by:</strong><br>Managing Director: Max Mustermann</p><p><strong>Contact:</strong><br>Phone: +49 (0) 123 456789<br>Email: info@breakpilot.app</p><p><strong>Register entry:</strong><br>Entry in the commercial register<br>Register court: Amtsgericht Musterstadt<br>Register number: HRB 12345</p><p><strong>VAT ID:</strong><br>VAT identification number according to § 27 a UStG: DE123456789</p><p><strong>Responsible for content according to § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>'
};
return imprint[lang] || imprint.de;
}
// Save cookie preferences
function saveCookiePreferences() {
const prefs = {};
const checkboxes = document.querySelectorAll('#cookie-categories-container input[type="checkbox"]');
checkboxes.forEach(cb => {
const name = cb.id.replace('cookie-', '');
if (name && !cb.disabled) {
prefs[name] = cb.checked;
}
});
localStorage.setItem('bp_cookie_consent', JSON.stringify(prefs));
localStorage.setItem('bp_cookie_consent_date', new Date().toISOString());
// TODO: Send to consent service if user is logged in
alert('Cookie-Einstellungen gespeichert!');
}
// ==========================================
// AUTH MODAL
// ==========================================
const authModal = document.getElementById('auth-modal');
const authModalClose = document.getElementById('auth-modal-close');
const authTabs = document.querySelectorAll('.auth-tab');
const authContents = document.querySelectorAll('.auth-content');
const btnLogin = document.getElementById('btn-login');
// Auth state
let currentUser = null;
let accessToken = localStorage.getItem('bp_access_token');
let refreshToken = localStorage.getItem('bp_refresh_token');
// Update UI based on auth state
function updateAuthUI() {
const loginBtn = document.getElementById('btn-login');
const userDropdown = document.querySelector('.auth-user-dropdown');
const notificationBell = document.getElementById('notification-bell');
if (currentUser && accessToken) {
// User is logged in - hide login button
if (loginBtn) loginBtn.style.display = 'none';
// Show notification bell
if (notificationBell) {
notificationBell.classList.add('active');
loadNotifications(); // Load notifications on login
startNotificationPolling(); // Start polling for new notifications
checkSuspensionStatus(); // Check if account is suspended
}
// Show user dropdown if it exists
if (userDropdown) {
userDropdown.classList.add('active');
const avatar = userDropdown.querySelector('.auth-user-avatar');
const menuName = userDropdown.querySelector('.auth-user-menu-name');
const menuEmail = userDropdown.querySelector('.auth-user-menu-email');
if (avatar) {
const initials = currentUser.name
? currentUser.name.substring(0, 2).toUpperCase()
: currentUser.email.substring(0, 2).toUpperCase();
avatar.textContent = initials;
}
if (menuName) menuName.textContent = currentUser.name || 'Benutzer';
if (menuEmail) menuEmail.textContent = currentUser.email;
}
} else {
// User is logged out - show login button
if (loginBtn) loginBtn.style.display = 'block';
if (userDropdown) userDropdown.classList.remove('active');
if (notificationBell) notificationBell.classList.remove('active');
stopNotificationPolling();
}
}
// Check if user is already logged in
async function checkAuthStatus() {
if (!accessToken) return;
try {
const response = await fetch('/api/auth/profile', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (response.ok) {
currentUser = await response.json();
updateAuthUI();
} else if (response.status === 401 && refreshToken) {
// Try to refresh the token
await refreshAccessToken();
} else {
// Clear invalid tokens
logout(false);
}
} catch (e) {
console.error('Auth check failed:', e);
}
}
// Refresh access token
async function refreshAccessToken() {
if (!refreshToken) return false;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (response.ok) {
const data = await response.json();
accessToken = data.access_token;
refreshToken = data.refresh_token;
currentUser = data.user;
localStorage.setItem('bp_access_token', accessToken);
localStorage.setItem('bp_refresh_token', refreshToken);
updateAuthUI();
return true;
} else {
logout(false);
return false;
}
} catch (e) {
console.error('Token refresh failed:', e);
return false;
}
}
// Logout
function logout(showMessage = true) {
if (refreshToken) {
fetch('/api/auth/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
}).catch(() => {});
}
currentUser = null;
accessToken = null;
refreshToken = null;
localStorage.removeItem('bp_access_token');
localStorage.removeItem('bp_refresh_token');
updateAuthUI();
if (showMessage) {
alert('Sie wurden erfolgreich abgemeldet.');
}
}
// Open auth modal
btnLogin?.addEventListener('click', () => {
authModal.classList.add('active');
showAuthTab('login');
clearAuthErrors();
});
// Close auth modal
authModalClose?.addEventListener('click', () => {
authModal.classList.remove('active');
clearAuthErrors();
});
// Close on background click
authModal?.addEventListener('click', (e) => {
if (e.target === authModal) {
authModal.classList.remove('active');
clearAuthErrors();
}
});
// Tab switching
authTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
showAuthTab(tabId);
});
});
function showAuthTab(tabId) {
authTabs.forEach(t => t.classList.remove('active'));
authContents.forEach(c => c.classList.remove('active'));
const activeTab = document.querySelector(`.auth-tab[data-tab="${tabId}"]`);
if (activeTab) activeTab.classList.add('active');
document.getElementById(`auth-${tabId}`)?.classList.add('active');
clearAuthErrors();
}
function clearAuthErrors() {
document.querySelectorAll('.auth-error, .auth-success').forEach(el => {
el.classList.remove('active');
el.textContent = '';
});
}
function showAuthError(elementId, message) {
const el = document.getElementById(elementId);
if (el) {
el.textContent = message;
el.classList.add('active');
}
}
function showAuthSuccess(elementId, message) {
const el = document.getElementById(elementId);
if (el) {
el.textContent = message;
el.classList.add('active');
}
}
// Login form
document.getElementById('auth-login-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const email = document.getElementById('login-email').value;
const password = document.getElementById('login-password').value;
const btn = document.getElementById('login-btn');
btn.disabled = true;
btn.textContent = 'Anmelden...';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
accessToken = data.access_token;
refreshToken = data.refresh_token;
currentUser = data.user;
localStorage.setItem('bp_access_token', accessToken);
localStorage.setItem('bp_refresh_token', refreshToken);
updateAuthUI();
authModal.classList.remove('active');
// Clear form
document.getElementById('login-email').value = '';
document.getElementById('login-password').value = '';
} else {
showAuthError('auth-login-error', data.detail || data.error || 'Anmeldung fehlgeschlagen');
}
} catch (e) {
showAuthError('auth-login-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Anmelden';
});
// Register form
document.getElementById('auth-register-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const name = document.getElementById('register-name').value;
const email = document.getElementById('register-email').value;
const password = document.getElementById('register-password').value;
const passwordConfirm = document.getElementById('register-password-confirm').value;
if (password !== passwordConfirm) {
showAuthError('auth-register-error', 'Passwörter stimmen nicht überein');
return;
}
if (password.length < 8) {
showAuthError('auth-register-error', 'Passwort muss mindestens 8 Zeichen lang sein');
return;
}
const btn = document.getElementById('register-btn');
btn.disabled = true;
btn.textContent = 'Registrieren...';
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name: name || undefined })
});
const data = await response.json();
if (response.ok) {
showAuthSuccess('auth-register-success',
'Registrierung erfolgreich! Bitte überprüfen Sie Ihre E-Mails zur Bestätigung.');
// Clear form
document.getElementById('register-name').value = '';
document.getElementById('register-email').value = '';
document.getElementById('register-password').value = '';
document.getElementById('register-password-confirm').value = '';
} else {
showAuthError('auth-register-error', data.detail || data.error || 'Registrierung fehlgeschlagen');
}
} catch (e) {
showAuthError('auth-register-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Registrieren';
});
// Forgot password form
document.getElementById('auth-forgot-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const email = document.getElementById('forgot-email').value;
const btn = document.getElementById('forgot-btn');
btn.disabled = true;
btn.textContent = 'Senden...';
try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
// Always show success to prevent email enumeration
showAuthSuccess('auth-forgot-success',
'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.');
document.getElementById('forgot-email').value = '';
} catch (e) {
showAuthError('auth-forgot-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Link senden';
});
// Reset password form
document.getElementById('auth-reset-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
clearAuthErrors();
const password = document.getElementById('reset-password').value;
const passwordConfirm = document.getElementById('reset-password-confirm').value;
const token = document.getElementById('reset-token').value;
if (password !== passwordConfirm) {
showAuthError('auth-reset-error', 'Passwörter stimmen nicht überein');
return;
}
if (password.length < 8) {
showAuthError('auth-reset-error', 'Passwort muss mindestens 8 Zeichen lang sein');
return;
}
const btn = document.getElementById('reset-btn');
btn.disabled = true;
btn.textContent = 'Ändern...';
try {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, new_password: password })
});
const data = await response.json();
if (response.ok) {
showAuthSuccess('auth-reset-success',
'Passwort erfolgreich geändert! Sie können sich jetzt anmelden.');
// Clear URL params
window.history.replaceState({}, document.title, window.location.pathname);
// Switch to login after 2 seconds
setTimeout(() => showAuthTab('login'), 2000);
} else {
showAuthError('auth-reset-error', data.detail || data.error || 'Passwort zurücksetzen fehlgeschlagen');
}
} catch (e) {
showAuthError('auth-reset-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
btn.disabled = false;
btn.textContent = 'Passwort ändern';
});
// Navigation links
document.getElementById('auth-forgot-password')?.addEventListener('click', (e) => {
e.preventDefault();
showAuthTab('forgot');
// Hide tabs for forgot password
document.querySelector('.auth-tabs').style.display = 'none';
});
document.getElementById('auth-back-to-login')?.addEventListener('click', (e) => {
e.preventDefault();
showAuthTab('login');
document.querySelector('.auth-tabs').style.display = 'flex';
});
document.getElementById('auth-goto-login')?.addEventListener('click', (e) => {
e.preventDefault();
showAuthTab('login');
});
// Check for URL parameters (email verification, password reset)
function checkAuthUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const verifyToken = urlParams.get('verify');
const resetToken = urlParams.get('reset');
if (verifyToken) {
authModal.classList.add('active');
document.querySelector('.auth-tabs').style.display = 'none';
showAuthTab('verify');
verifyEmail(verifyToken);
} else if (resetToken) {
authModal.classList.add('active');
document.querySelector('.auth-tabs').style.display = 'none';
showAuthTab('reset');
document.getElementById('reset-token').value = resetToken;
}
}
async function verifyEmail(token) {
try {
const response = await fetch('/api/auth/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const data = await response.json();
const loadingEl = document.getElementById('auth-verify-loading');
if (response.ok) {
if (loadingEl) loadingEl.style.display = 'none';
showAuthSuccess('auth-verify-success', 'E-Mail erfolgreich verifiziert! Sie können sich jetzt anmelden.');
// Clear URL params
window.history.replaceState({}, document.title, window.location.pathname);
// Switch to login after 2 seconds
setTimeout(() => {
showAuthTab('login');
document.querySelector('.auth-tabs').style.display = 'flex';
}, 2000);
} else {
if (loadingEl) loadingEl.style.display = 'none';
showAuthError('auth-verify-error', data.detail || data.error || 'Verifizierung fehlgeschlagen. Der Link ist möglicherweise abgelaufen.');
}
} catch (e) {
document.getElementById('auth-verify-loading').style.display = 'none';
showAuthError('auth-verify-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
}
}
// User dropdown toggle
const authUserBtn = document.getElementById('auth-user-btn');
const authUserMenu = document.getElementById('auth-user-menu');
authUserBtn?.addEventListener('click', (e) => {
e.stopPropagation();
authUserMenu.classList.toggle('active');
});
// Close dropdown when clicking outside
document.addEventListener('click', () => {
authUserMenu?.classList.remove('active');
});
// Placeholder functions for profile/sessions
function showProfileModal() {
alert('Profil-Einstellungen kommen bald!');
}
function showSessionsModal() {
alert('Sitzungsverwaltung kommt bald!');
}
// ==========================================
// NOTIFICATION FUNCTIONS
// ==========================================
let notificationPollingInterval = null;
let notificationOffset = 0;
let notificationPrefs = {
email_enabled: true,
push_enabled: false,
in_app_enabled: true
};
// Toggle notification panel
document.getElementById('notification-bell-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
const panel = document.getElementById('notification-panel');
panel.classList.toggle('active');
// Close user menu if open
const userMenu = document.getElementById('auth-user-menu');
userMenu?.classList.remove('active');
});
// Close notification panel when clicking outside
document.addEventListener('click', (e) => {
const bell = document.getElementById('notification-bell');
const panel = document.getElementById('notification-panel');
if (bell && panel && !bell.contains(e.target)) {
panel.classList.remove('active');
}
});
// Load notifications from API
async function loadNotifications(append = false) {
if (!accessToken) return;
try {
const limit = 10;
const offset = append ? notificationOffset : 0;
const response = await fetch(`/api/v1/notifications?limit=${limit}&offset=${offset}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const data = await response.json();
if (!append) {
notificationOffset = 0;
}
notificationOffset += data.notifications?.length || 0;
renderNotifications(data.notifications || [], data.total || 0, append);
updateNotificationBadge();
} catch (e) {
console.error('Failed to load notifications:', e);
}
}
// Render notifications in the panel
function renderNotifications(notifications, total, append = false) {
const list = document.getElementById('notification-list');
if (!list) return;
if (!append) {
list.innerHTML = '';
}
if (notifications.length === 0 && !append) {
list.innerHTML = `
<div class="notification-empty">
<div class="notification-empty-icon">🔔</div>
<div>Keine Benachrichtigungen</div>
</div>
`;
return;
}
notifications.forEach(n => {
const item = document.createElement('div');
item.className = `notification-item ${!n.read_at ? 'unread' : ''}`;
item.onclick = () => markNotificationRead(n.id);
const icon = getNotificationIcon(n.type);
const timeAgo = formatTimeAgo(new Date(n.created_at));
item.innerHTML = `
<div class="notification-icon">${icon}</div>
<div class="notification-content">
<div class="notification-title">${escapeHtml(n.title)}</div>
<div class="notification-body">${escapeHtml(n.body)}</div>
<div class="notification-time">${timeAgo}</div>
</div>
`;
list.appendChild(item);
});
// Show/hide load more button
const footer = document.querySelector('.notification-footer');
if (footer) {
footer.style.display = notificationOffset < total ? 'block' : 'none';
}
}
// Get icon for notification type
function getNotificationIcon(type) {
const icons = {
'consent_required': '📋',
'consent_reminder': '⏰',
'version_published': '📢',
'version_approved': '✅',
'version_rejected': '❌',
'account_suspended': '🚫',
'account_restored': '🔓',
'general': '🔔'
};
return icons[type] || '🔔';
}
// Format time ago
function formatTimeAgo(date) {
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Gerade eben';
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`;
if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`;
if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`;
return date.toLocaleDateString('de-DE');
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Update notification badge
async function updateNotificationBadge() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/notifications/unread-count', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const data = await response.json();
const badge = document.getElementById('notification-badge');
if (badge) {
const count = data.unread_count || 0;
badge.textContent = count > 99 ? '99+' : count;
badge.classList.toggle('hidden', count === 0);
}
} catch (e) {
console.error('Failed to update badge:', e);
}
}
// Mark notification as read
async function markNotificationRead(id) {
if (!accessToken) return;
try {
await fetch(`/api/v1/notifications/${id}/read`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// Update UI
const item = document.querySelector(`.notification-item[onclick*="${id}"]`);
if (item) item.classList.remove('unread');
updateNotificationBadge();
} catch (e) {
console.error('Failed to mark notification as read:', e);
}
}
// Mark all notifications as read
async function markAllNotificationsRead() {
if (!accessToken) return;
try {
await fetch('/api/v1/notifications/read-all', {
method: 'PUT',
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// Update UI
document.querySelectorAll('.notification-item.unread').forEach(item => {
item.classList.remove('unread');
});
updateNotificationBadge();
} catch (e) {
console.error('Failed to mark all as read:', e);
}
}
// Load more notifications
function loadMoreNotifications() {
loadNotifications(true);
}
// Start polling for new notifications
function startNotificationPolling() {
stopNotificationPolling();
notificationPollingInterval = setInterval(() => {
updateNotificationBadge();
}, 30000); // Poll every 30 seconds
}
// Stop polling
function stopNotificationPolling() {
if (notificationPollingInterval) {
clearInterval(notificationPollingInterval);
notificationPollingInterval = null;
}
}
// Show notification preferences modal
function showNotificationPreferences() {
document.getElementById('notification-panel')?.classList.remove('active');
document.getElementById('notification-prefs-modal')?.classList.add('active');
loadNotificationPreferences();
}
// Close notification preferences modal
function closeNotificationPreferences() {
document.getElementById('notification-prefs-modal')?.classList.remove('active');
}
// Load notification preferences
async function loadNotificationPreferences() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/notifications/preferences', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const prefs = await response.json();
notificationPrefs = prefs;
// Update UI toggles
updateToggle('pref-email-toggle', prefs.email_enabled);
updateToggle('pref-inapp-toggle', prefs.in_app_enabled);
updateToggle('pref-push-toggle', prefs.push_enabled);
} catch (e) {
console.error('Failed to load preferences:', e);
}
}
// Update toggle UI
function updateToggle(id, active) {
const toggle = document.getElementById(id);
if (toggle) {
toggle.classList.toggle('active', active);
}
}
// Toggle notification preference
function toggleNotificationPref(type) {
const toggleMap = {
'email': 'pref-email-toggle',
'inapp': 'pref-inapp-toggle',
'push': 'pref-push-toggle'
};
const prefMap = {
'email': 'email_enabled',
'inapp': 'in_app_enabled',
'push': 'push_enabled'
};
const toggleId = toggleMap[type];
const prefKey = prefMap[type];
const toggle = document.getElementById(toggleId);
if (toggle) {
const isActive = toggle.classList.toggle('active');
notificationPrefs[prefKey] = isActive;
}
}
// Save notification preferences
async function saveNotificationPreferences() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/notifications/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(notificationPrefs)
});
if (response.ok) {
closeNotificationPreferences();
alert('Einstellungen gespeichert!');
} else {
alert('Fehler beim Speichern der Einstellungen');
}
} catch (e) {
console.error('Failed to save preferences:', e);
alert('Fehler beim Speichern der Einstellungen');
}
}
// ==========================================
// SUSPENSION CHECK FUNCTIONS
// ==========================================
let isSuspended = false;
// Check suspension status after login
async function checkSuspensionStatus() {
if (!accessToken) return;
try {
const response = await fetch('/api/v1/account/suspension-status', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!response.ok) return;
const data = await response.json();
if (data.suspended) {
isSuspended = true;
showSuspensionOverlay(data);
} else {
isSuspended = false;
hideSuspensionOverlay();
}
} catch (e) {
console.error('Failed to check suspension status:', e);
}
}
// Show suspension overlay
function showSuspensionOverlay(data) {
const overlay = document.getElementById('suspension-overlay');
const docList = document.getElementById('suspension-doc-list');
if (!overlay || !docList) return;
// Populate document list
if (data.pending_deadlines && data.pending_deadlines.length > 0) {
docList.innerHTML = data.pending_deadlines.map(d => {
const deadline = new Date(d.deadline_at);
const isOverdue = deadline < new Date();
return `
<div class="suspension-doc-item">
<span class="suspension-doc-name">${escapeHtml(d.document_name)}</span>
<span class="suspension-doc-deadline">${isOverdue ? 'Überfällig' : deadline.toLocaleDateString('de-DE')}</span>
</div>
`;
}).join('');
} else if (data.details && data.details.documents) {
docList.innerHTML = data.details.documents.map(doc => `
<div class="suspension-doc-item">
<span class="suspension-doc-name">${escapeHtml(doc)}</span>
<span class="suspension-doc-deadline">Bestätigung erforderlich</span>
</div>
`).join('');
}
overlay.classList.add('active');
}
// Hide suspension overlay
function hideSuspensionOverlay() {
const overlay = document.getElementById('suspension-overlay');
if (overlay) {
overlay.classList.remove('active');
}
}
// Show consent modal from suspension overlay
function showConsentModal() {
hideSuspensionOverlay();
// Open legal modal to consent tab
document.getElementById('legal-modal')?.classList.add('active');
// Switch to appropriate tab
}
// Initialize auth on page load
checkAuthStatus();
checkAuthUrlParams();
// ==========================================
// RICH TEXT EDITOR FUNCTIONS
// ==========================================
const versionEditor = document.getElementById('admin-version-editor');
const versionContentHidden = document.getElementById('admin-version-content');
const editorCharCount = document.getElementById('editor-char-count');
// Update hidden field and char count when editor content changes
versionEditor?.addEventListener('input', () => {
versionContentHidden.value = versionEditor.innerHTML;
const textLength = versionEditor.textContent.length;
editorCharCount.textContent = `${textLength} Zeichen`;
});
// Format document with execCommand
function formatDoc(cmd, value = null) {
versionEditor.focus();
document.execCommand(cmd, false, value);
}
// Format block element
function formatBlock(tag) {
versionEditor.focus();
document.execCommand('formatBlock', false, `<${tag}>`);
}
// Insert link
function insertLink() {
const url = prompt('Link-URL eingeben:', 'https://');
if (url) {
versionEditor.focus();
document.execCommand('createLink', false, url);
}
}
// Handle Word document upload
async function handleWordUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Show loading indicator
const editor = document.getElementById('admin-version-editor');
const originalContent = editor.innerHTML;
editor.innerHTML = '<p style="color: var(--bp-text-muted); text-align: center; padding: 40px;">Word-Dokument wird verarbeitet...</p>';
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/consent/admin/versions/upload-word', {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
editor.innerHTML = data.html || '<p>Konvertierung fehlgeschlagen</p>';
versionContentHidden.value = editor.innerHTML;
// Update char count
const textLength = editor.textContent.length;
editorCharCount.textContent = `${textLength} Zeichen`;
} else {
const error = await response.json();
editor.innerHTML = originalContent;
alert('Fehler beim Importieren: ' + (error.detail || 'Unbekannter Fehler'));
}
} catch (e) {
editor.innerHTML = originalContent;
alert('Fehler beim Hochladen: ' + e.message);
}
// Reset file input
event.target.value = '';
}
// Handle paste from Word - clean up the HTML
versionEditor?.addEventListener('paste', (e) => {
// Get pasted data via clipboard API
const clipboardData = e.clipboardData || window.clipboardData;
const pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain');
if (pastedData && clipboardData.getData('text/html')) {
e.preventDefault();
// Clean the HTML
const cleanHtml = cleanWordHtml(pastedData);
document.execCommand('insertHTML', false, cleanHtml);
// Update hidden field
versionContentHidden.value = versionEditor.innerHTML;
}
});
// Clean Word-specific HTML
function cleanWordHtml(html) {
// Create a temporary container
const temp = document.createElement('div');
temp.innerHTML = html;
// Remove Word-specific elements and attributes
const elementsToRemove = temp.querySelectorAll('style, script, meta, link, xml');
elementsToRemove.forEach(el => el.remove());
// Get text content from Word spans with specific styling
let cleanedHtml = temp.innerHTML;
// Remove mso-* styles and other Office-specific CSS
cleanedHtml = cleanedHtml.replace(/\s*mso-[^:]+:[^;]+;?/gi, '');
cleanedHtml = cleanedHtml.replace(/\s*style="[^"]*"/gi, '');
cleanedHtml = cleanedHtml.replace(/\s*class="[^"]*"/gi, '');
cleanedHtml = cleanedHtml.replace(/<o:p><\/o:p>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?o:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?w:[^>]*>/gi, '');
cleanedHtml = cleanedHtml.replace(/<\/?m:[^>]*>/gi, '');
// Clean up empty spans
cleanedHtml = cleanedHtml.replace(/<span[^>]*>\s*<\/span>/gi, '');
// Convert Word list markers to proper lists
cleanedHtml = cleanedHtml.replace(/<p[^>]*>\s*[•·]\s*/gi, '<li>');
return cleanedHtml;
}
// ==========================================
// ADMIN PANEL
// ==========================================
const adminModal = document.getElementById('admin-modal');
const adminModalClose = document.getElementById('admin-modal-close');
const adminTabs = document.querySelectorAll('.admin-tab');
const adminContents = document.querySelectorAll('.admin-content');
const btnAdmin = document.getElementById('btn-admin');
// Admin data cache
let adminDocuments = [];
let adminCookieCategories = [];
// Open admin modal
btnAdmin?.addEventListener('click', async () => {
adminModal.classList.add('active');
await loadAdminDocuments();
await loadAdminCookieCategories();
populateDocumentSelect();
});
// Close admin modal
adminModalClose?.addEventListener('click', () => {
adminModal.classList.remove('active');
});
// Close on background click
adminModal?.addEventListener('click', (e) => {
if (e.target === adminModal) {
adminModal.classList.remove('active');
}
});
// Admin tab switching
adminTabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.dataset.tab;
adminTabs.forEach(t => t.classList.remove('active'));
adminContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`admin-${tabId}`)?.classList.add('active');
// Load stats when stats tab is clicked
if (tabId === 'stats') {
loadAdminStats();
}
});
});
// ==========================================
// DOCUMENTS MANAGEMENT
// ==========================================
async function loadAdminDocuments() {
const container = document.getElementById('admin-doc-table-container');
container.innerHTML = '<div class="admin-loading">Lade Dokumente...</div>';
try {
const res = await fetch('/api/consent/admin/documents');
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
adminDocuments = data.documents || [];
renderDocumentsTable();
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Dokumente.</div>';
}
}
function renderDocumentsTable() {
const container = document.getElementById('admin-doc-table-container');
// Alle Dokumente anzeigen
const allDocs = adminDocuments;
if (allDocs.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Dokumente vorhanden. Klicken Sie auf "+ Neues Dokument" um ein Dokument zu erstellen.</div>';
return;
}
const typeLabels = {
'terms': 'AGB',
'privacy': 'Datenschutz',
'cookies': 'Cookies',
'community': 'Community',
'imprint': 'Impressum'
};
const html = `
<table class="admin-table">
<thead>
<tr>
<th>Typ</th>
<th>Name</th>
<th>Beschreibung</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${allDocs.map(doc => `
<tr>
<td><span class="admin-badge admin-badge-${doc.is_mandatory ? 'mandatory' : 'optional'}">${typeLabels[doc.type] || doc.type}</span></td>
<td>${doc.name}</td>
<td style="color: var(--bp-text-muted); font-size: 12px;">${doc.description || '-'}</td>
<td>
${doc.is_active ? '<span class="admin-badge admin-badge-published">Aktiv</span>' : '<span class="admin-badge admin-badge-draft">Inaktiv</span>'}
${doc.is_mandatory ? '<span class="admin-badge admin-badge-mandatory" style="margin-left: 4px;">Pflicht</span>' : ''}
</td>
<td class="admin-actions">
<button class="admin-btn admin-btn-secondary" onclick="editDocument('${doc.id}')" title="Bearbeiten">✏️</button>
<button class="admin-btn admin-btn-primary" onclick="goToVersions('${doc.id}')">Versionen</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = html;
}
function goToVersions(docId) {
// Wechsle zum Versionen-Tab und wähle das Dokument aus
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
if (versionsTab) {
versionsTab.click();
setTimeout(() => {
const select = document.getElementById('admin-version-doc-select');
if (select) {
select.value = docId;
loadVersionsForDocument();
}
}, 100);
}
}
function showDocumentForm(doc = null) {
const form = document.getElementById('admin-document-form');
const title = document.getElementById('admin-document-form-title');
if (doc) {
title.textContent = 'Dokument bearbeiten';
document.getElementById('admin-document-id').value = doc.id;
document.getElementById('admin-document-type').value = doc.type;
document.getElementById('admin-document-name').value = doc.name;
document.getElementById('admin-document-description').value = doc.description || '';
document.getElementById('admin-document-mandatory').checked = doc.is_mandatory;
} else {
title.textContent = 'Neues Dokument erstellen';
document.getElementById('admin-document-id').value = '';
document.getElementById('admin-document-type').value = '';
document.getElementById('admin-document-name').value = '';
document.getElementById('admin-document-description').value = '';
document.getElementById('admin-document-mandatory').checked = true;
}
form.style.display = 'block';
}
function hideDocumentForm() {
document.getElementById('admin-document-form').style.display = 'none';
}
function editDocument(docId) {
const doc = adminDocuments.find(d => d.id === docId);
if (doc) showDocumentForm(doc);
}
async function saveDocument() {
const docId = document.getElementById('admin-document-id').value;
const docType = document.getElementById('admin-document-type').value;
const docName = document.getElementById('admin-document-name').value;
if (!docType || !docName) {
alert('Bitte füllen Sie alle Pflichtfelder aus (Typ und Name).');
return;
}
const data = {
type: docType,
name: docName,
description: document.getElementById('admin-document-description').value || null,
is_mandatory: document.getElementById('admin-document-mandatory').checked
};
try {
const url = docId ? `/api/consent/admin/documents/${docId}` : '/api/consent/admin/documents';
const method = docId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to save');
hideDocumentForm();
await loadAdminDocuments();
populateDocumentSelect();
alert('Dokument gespeichert!');
} catch(e) {
alert('Fehler beim Speichern: ' + e.message);
}
}
async function deleteDocument(docId) {
if (!confirm('Dokument wirklich deaktivieren?')) return;
try {
const res = await fetch(`/api/consent/admin/documents/${docId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete');
await loadAdminDocuments();
populateDocumentSelect();
alert('Dokument deaktiviert!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// ==========================================
// VERSIONS MANAGEMENT
// ==========================================
function populateDocumentSelect() {
const select = document.getElementById('admin-version-doc-select');
const uniqueDocs = [...new Map(adminDocuments.map(d => [d.type, d])).values()];
select.innerHTML = '<option value="">-- Dokument auswählen --</option>' +
adminDocuments.filter(d => d.is_active).map(doc =>
`<option value="${doc.id}">${doc.name} (${doc.type})</option>`
).join('');
}
async function loadVersionsForDocument() {
const docId = document.getElementById('admin-version-doc-select').value;
const container = document.getElementById('admin-version-table-container');
const btnNew = document.getElementById('btn-new-version');
if (!docId) {
container.innerHTML = '<div class="admin-empty">Wählen Sie ein Dokument aus.</div>';
btnNew.disabled = true;
return;
}
btnNew.disabled = false;
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
renderVersionsTable(data.versions || []);
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
}
}
function renderVersionsTable(versions) {
const container = document.getElementById('admin-version-table-container');
if (versions.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden.</div>';
return;
}
const getStatusBadge = (status) => {
const statusLabels = {
'draft': 'Entwurf',
'review': 'In Prüfung',
'approved': 'Genehmigt',
'rejected': 'Abgelehnt',
'scheduled': 'Geplant',
'published': 'Veröffentlicht',
'archived': 'Archiviert'
};
return statusLabels[status] || status;
};
const formatScheduledDate = (isoDate) => {
if (!isoDate) return '';
const date = new Date(isoDate);
return date.toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
};
const html = `
<table class="admin-table">
<thead>
<tr>
<th>Version</th>
<th>Sprache</th>
<th>Titel</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${versions.map(v => `
<tr>
<td>${v.version}</td>
<td>${v.language.toUpperCase()}</td>
<td>${v.title}</td>
<td>
<span class="admin-badge admin-badge-${v.status}">${getStatusBadge(v.status)}</span>
${v.scheduled_publish_at ? `<br><small>Geplant: ${formatScheduledDate(v.scheduled_publish_at)}</small>` : ''}
</td>
<td class="admin-actions">
${v.status === 'draft' ? `
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
<button class="admin-btn admin-btn-primary" onclick="submitForReview('${v.id}')">Zur Prüfung</button>
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
` : ''}
${v.status === 'review' ? `
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<button class="admin-btn admin-btn-primary" onclick="showApprovalDialog('${v.id}')">Genehmigen</button>
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Ablehnen</button>
` : ''}
${v.status === 'rejected' ? `
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
` : ''}
${v.status === 'scheduled' ? `
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<span class="admin-info-text">Wartet auf Veröffentlichung</span>
` : ''}
${v.status === 'approved' ? `
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
<button class="admin-btn admin-btn-publish" onclick="publishVersion('${v.id}')">Sofort veröffentlichen</button>
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Zurücksetzen</button>
` : ''}
${v.status === 'published' ? `
<button class="admin-btn admin-btn-delete" onclick="archiveVersion('${v.id}')">Archivieren</button>
` : ''}
<button class="admin-btn" onclick="showApprovalHistory('${v.id}')">Historie</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = html;
}
function showVersionForm() {
const form = document.getElementById('admin-version-form');
document.getElementById('admin-version-id').value = '';
document.getElementById('admin-version-number').value = '';
document.getElementById('admin-version-lang').value = 'de';
document.getElementById('admin-version-title').value = '';
document.getElementById('admin-version-summary').value = '';
document.getElementById('admin-version-content').value = '';
// Clear rich text editor
const editor = document.getElementById('admin-version-editor');
if (editor) {
editor.innerHTML = '';
document.getElementById('editor-char-count').textContent = '0 Zeichen';
}
form.classList.add('active');
}
function hideVersionForm() {
document.getElementById('admin-version-form').classList.remove('active');
}
async function editVersion(versionId) {
// Lade die Version und fülle das Formular
const docId = document.getElementById('admin-version-doc-select').value;
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Failed to load versions');
const data = await res.json();
const version = (data.versions || []).find(v => v.id === versionId);
if (!version) {
alert('Version nicht gefunden');
return;
}
// Formular öffnen und Daten einfügen
const form = document.getElementById('admin-version-form');
document.getElementById('admin-version-id').value = version.id;
document.getElementById('admin-version-number').value = version.version;
document.getElementById('admin-version-lang').value = version.language;
document.getElementById('admin-version-title').value = version.title;
document.getElementById('admin-version-summary').value = version.summary || '';
// Rich-Text-Editor mit Inhalt füllen
const editor = document.getElementById('admin-version-editor');
if (editor) {
editor.innerHTML = version.content || '';
const charCount = editor.textContent.length;
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
}
document.getElementById('admin-version-content').value = version.content || '';
form.classList.add('active');
} catch(e) {
alert('Fehler beim Laden der Version: ' + e.message);
}
}
async function saveVersion() {
const docId = document.getElementById('admin-version-doc-select').value;
const versionId = document.getElementById('admin-version-id').value;
// Get content from rich text editor
const editor = document.getElementById('admin-version-editor');
const content = editor ? editor.innerHTML : document.getElementById('admin-version-content').value;
const data = {
document_id: docId,
version: document.getElementById('admin-version-number').value,
language: document.getElementById('admin-version-lang').value,
title: document.getElementById('admin-version-title').value,
summary: document.getElementById('admin-version-summary').value,
content: content
};
try {
const url = versionId ? `/api/consent/admin/versions/${versionId}` : '/api/consent/admin/versions';
const method = versionId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to save');
hideVersionForm();
await loadVersionsForDocument();
alert('Version gespeichert!');
} catch(e) {
alert('Fehler beim Speichern: ' + e.message);
}
}
async function publishVersion(versionId) {
if (!confirm('Version wirklich veröffentlichen?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
if (!res.ok) throw new Error('Failed to publish');
await loadVersionsForDocument();
alert('Version veröffentlicht!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function archiveVersion(versionId) {
if (!confirm('Version wirklich archivieren?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/archive`, { method: 'POST' });
if (!res.ok) throw new Error('Failed to archive');
await loadVersionsForDocument();
alert('Version archiviert!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function deleteVersion(versionId) {
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei und kann erneut verwendet werden.\\n\\nDiese Aktion kann nicht rückgängig gemacht werden!')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
}
await loadVersionsForDocument();
alert('Version wurde dauerhaft gelöscht.');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// ==========================================
// DSB APPROVAL WORKFLOW
// ==========================================
async function submitForReview(versionId) {
if (!confirm('Version zur DSB-Prüfung einreichen?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/submit-review`, { method: 'POST' });
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail?.error || 'Einreichung fehlgeschlagen');
}
await loadVersionsForDocument();
alert('Version wurde zur Prüfung eingereicht!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Dialog für Genehmigung mit Veröffentlichungszeitpunkt
let approvalVersionId = null;
function showApprovalDialog(versionId) {
approvalVersionId = versionId;
const dialog = document.getElementById('approval-dialog');
// Setze Minimum-Datum auf morgen
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
document.getElementById('approval-date').min = tomorrow.toISOString().split('T')[0];
document.getElementById('approval-date').value = '';
document.getElementById('approval-time').value = '00:00';
document.getElementById('approval-comment').value = '';
dialog.classList.add('active');
}
function hideApprovalDialog() {
document.getElementById('approval-dialog').classList.remove('active');
approvalVersionId = null;
}
async function submitApproval() {
if (!approvalVersionId) return;
const dateInput = document.getElementById('approval-date').value;
const timeInput = document.getElementById('approval-time').value;
const comment = document.getElementById('approval-comment').value;
let scheduledPublishAt = null;
if (dateInput) {
// Kombiniere Datum und Zeit zu ISO 8601
const datetime = new Date(dateInput + 'T' + (timeInput || '00:00') + ':00');
scheduledPublishAt = datetime.toISOString();
}
try {
const body = { comment: comment || '' };
if (scheduledPublishAt) {
body.scheduled_publish_at = scheduledPublishAt;
}
const res = await fetch(`/api/consent/admin/versions/${approvalVersionId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail?.error || data.detail || 'Genehmigung fehlgeschlagen');
}
hideApprovalDialog();
await loadVersionsForDocument();
if (scheduledPublishAt) {
const date = new Date(scheduledPublishAt);
alert('Version genehmigt! Geplante Veröffentlichung: ' + date.toLocaleString('de-DE'));
} else {
alert('Version genehmigt! Sie kann jetzt manuell veröffentlicht werden.');
}
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Alte Funktion für Rückwärtskompatibilität
async function approveVersion(versionId) {
showApprovalDialog(versionId);
}
async function rejectVersion(versionId) {
const comment = prompt('Begründung für Ablehnung (erforderlich):');
if (!comment) {
alert('Eine Begründung ist erforderlich.');
return;
}
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment })
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail?.error || data.detail || 'Ablehnung fehlgeschlagen');
}
await loadVersionsForDocument();
alert('Version abgelehnt und zurück in Entwurf-Status versetzt.');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Store current compare version for actions
let currentCompareVersionId = null;
let currentCompareVersionStatus = null;
let currentCompareDocId = null;
async function showCompareView(versionId) {
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/compare`);
if (!res.ok) throw new Error('Vergleich konnte nicht geladen werden');
const data = await res.json();
const currentVersion = data.current_version;
const publishedVersion = data.published_version;
const history = data.approval_history || [];
// Store version info for actions
currentCompareVersionId = versionId;
currentCompareVersionStatus = currentVersion.status;
currentCompareDocId = currentVersion.document_id;
// Update header info
document.getElementById('compare-published-info').textContent =
publishedVersion ? `${publishedVersion.title} (v${publishedVersion.version})` : 'Keine Version';
document.getElementById('compare-draft-info').textContent =
`${currentVersion.title} (v${currentVersion.version})`;
document.getElementById('compare-published-version').textContent =
publishedVersion ? `v${publishedVersion.version}` : '';
document.getElementById('compare-draft-version').textContent =
`v${currentVersion.version} - ${currentVersion.status}`;
// Populate content panels
const leftPanel = document.getElementById('compare-content-left');
const rightPanel = document.getElementById('compare-content-right');
leftPanel.innerHTML = publishedVersion
? publishedVersion.content
: '<div class="no-content">Keine veröffentlichte Version vorhanden</div>';
rightPanel.innerHTML = currentVersion.content || '<div class="no-content">Kein Inhalt</div>';
// Populate history
const historyContainer = document.getElementById('compare-history-container');
if (history.length > 0) {
historyContainer.innerHTML = `
<div class="compare-history-title">Genehmigungsverlauf</div>
<div class="compare-history-list">
${history.map(h => `
<span class="compare-history-item">
<strong>${h.action}</strong> von ${h.approver || 'System'}
(${new Date(h.created_at).toLocaleString('de-DE')})
${h.comment ? ': ' + h.comment : ''}
</span>
`).join(' | ')}
</div>
`;
} else {
historyContainer.innerHTML = '';
}
// Render action buttons based on status
renderCompareActions(currentVersion.status, versionId);
// Setup synchronized scrolling
setupSyncScroll(leftPanel, rightPanel);
// Show the overlay
document.getElementById('version-compare-view').classList.add('active');
document.body.style.overflow = 'hidden';
} catch(e) {
alert('Fehler beim Laden des Vergleichs: ' + e.message);
}
}
function renderCompareActions(status, versionId) {
const actionsContainer = document.getElementById('compare-actions-container');
let buttons = '';
// Edit button - available for draft, review, and rejected
if (status === 'draft' || status === 'review' || status === 'rejected') {
buttons += `<button class="btn btn-primary" onclick="editVersionFromCompare('${versionId}')">
✏️ Version bearbeiten
</button>`;
}
// Status-specific actions
if (status === 'draft') {
buttons += `<button class="btn" onclick="submitForReviewFromCompare('${versionId}')">
📤 Zur Prüfung einreichen
</button>`;
}
if (status === 'review') {
buttons += `<button class="btn btn-success" onclick="approveVersionFromCompare('${versionId}')">
✅ Genehmigen
</button>`;
buttons += `<button class="btn btn-danger" onclick="rejectVersionFromCompare('${versionId}')">
❌ Ablehnen
</button>`;
}
if (status === 'approved') {
buttons += `<button class="btn btn-primary" onclick="publishVersionFromCompare('${versionId}')">
🚀 Veröffentlichen
</button>`;
}
// Delete button for draft/rejected
if (status === 'draft' || status === 'rejected') {
buttons += `<button class="btn btn-danger" onclick="deleteVersionFromCompare('${versionId}')" style="margin-left: auto;">
🗑️ Löschen
</button>`;
}
actionsContainer.innerHTML = buttons;
}
async function editVersionFromCompare(versionId) {
// Store the doc ID before closing compare view
const docId = currentCompareDocId;
// Close compare view
hideCompareView();
// Switch to versions tab
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
if (versionsTab) {
versionsTab.click();
}
// Wait a moment for the tab to become active
await new Promise(resolve => setTimeout(resolve, 150));
// Ensure document select is populated
populateDocumentSelect();
// Set the document select if we have the doc ID
if (docId) {
const select = document.getElementById('admin-version-doc-select');
if (select) {
select.value = docId;
// Load versions for this document
await loadVersionsForDocument();
}
}
// Now load the version data directly and open the form
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Failed to load versions');
const data = await res.json();
const version = (data.versions || []).find(v => v.id === versionId);
if (!version) {
alert('Version nicht gefunden');
return;
}
// Open the form and fill with version data
const form = document.getElementById('admin-version-form');
document.getElementById('admin-version-id').value = version.id;
document.getElementById('admin-version-number').value = version.version;
document.getElementById('admin-version-lang').value = version.language;
document.getElementById('admin-version-title').value = version.title;
document.getElementById('admin-version-summary').value = version.summary || '';
// Fill rich text editor with content
const editor = document.getElementById('admin-version-editor');
if (editor) {
editor.innerHTML = version.content || '';
const charCount = editor.textContent.length;
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
}
document.getElementById('admin-version-content').value = version.content || '';
form.classList.add('active');
} catch(e) {
alert('Fehler beim Laden der Version: ' + e.message);
}
}
async function submitForReviewFromCompare(versionId) {
await submitForReview(versionId);
hideCompareView();
await loadVersionsForDocument();
}
async function approveVersionFromCompare(versionId) {
const comment = prompt('Kommentar zur Genehmigung (optional):');
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment || '' })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || err.error || 'Genehmigung fehlgeschlagen');
}
alert('Version genehmigt!');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function rejectVersionFromCompare(versionId) {
const comment = prompt('Begründung für die Ablehnung (erforderlich):');
if (!comment) {
alert('Eine Begründung ist erforderlich.');
return;
}
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment })
});
if (!res.ok) throw new Error('Ablehnung fehlgeschlagen');
alert('Version abgelehnt. Der Autor kann sie überarbeiten.');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function publishVersionFromCompare(versionId) {
if (!confirm('Version wirklich veröffentlichen?')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
if (!res.ok) throw new Error('Veröffentlichung fehlgeschlagen');
alert('Version veröffentlicht!');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function deleteVersionFromCompare(versionId) {
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei.')) return;
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
}
alert('Version gelöscht!');
hideCompareView();
await loadVersionsForDocument();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
function hideCompareView() {
document.getElementById('version-compare-view').classList.remove('active');
document.body.style.overflow = '';
// Remove scroll listeners
const leftPanel = document.getElementById('compare-content-left');
const rightPanel = document.getElementById('compare-content-right');
if (leftPanel) leftPanel.onscroll = null;
if (rightPanel) rightPanel.onscroll = null;
}
// Synchronized scrolling between two panels
let syncScrollActive = false;
function setupSyncScroll(leftPanel, rightPanel) {
// Remove any existing listeners first
leftPanel.onscroll = null;
rightPanel.onscroll = null;
// Flag to prevent infinite scroll loops
let isScrolling = false;
rightPanel.onscroll = function() {
if (isScrolling) return;
isScrolling = true;
// Calculate scroll percentage
const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight);
// Apply same percentage to left panel
const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight;
leftPanel.scrollTop = rightScrollPercent * leftMaxScroll;
setTimeout(() => { isScrolling = false; }, 10);
};
leftPanel.onscroll = function() {
if (isScrolling) return;
isScrolling = true;
// Calculate scroll percentage
const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight);
// Apply same percentage to right panel
const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight;
rightPanel.scrollTop = leftScrollPercent * rightMaxScroll;
setTimeout(() => { isScrolling = false; }, 10);
};
}
async function showApprovalHistory(versionId) {
try {
const res = await fetch(`/api/consent/admin/versions/${versionId}/approval-history`);
if (!res.ok) throw new Error('Historie konnte nicht geladen werden');
const data = await res.json();
const history = data.approval_history || [];
const content = history.length === 0
? '<p>Keine Genehmigungshistorie vorhanden.</p>'
: `
<table class="admin-table" style="font-size: 14px;">
<thead>
<tr>
<th>Aktion</th>
<th>Benutzer</th>
<th>Kommentar</th>
<th>Datum</th>
</tr>
</thead>
<tbody>
${history.map(h => `
<tr>
<td><span class="admin-badge admin-badge-${h.action}">${h.action}</span></td>
<td>${h.approver || h.name || '-'}</td>
<td>${h.comment || '-'}</td>
<td>${new Date(h.created_at).toLocaleString('de-DE')}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
showCustomModal('Genehmigungsverlauf', content, [
{ text: 'Schließen', onClick: () => hideCustomModal() }
]);
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// Custom Modal Functions
function showCustomModal(title, content, buttons = []) {
let modal = document.getElementById('custom-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'custom-modal';
modal.className = 'modal-overlay';
document.body.appendChild(modal);
}
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; width: 95%;">
<div class="modal-header">
<h2>${title}</h2>
<button class="modal-close" onclick="hideCustomModal()">&times;</button>
</div>
<div class="modal-body" style="max-height: 80vh; overflow-y: auto;">
${content}
</div>
${buttons.length > 0 ? `
<div class="modal-footer" style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;">
${buttons.map(b => `
<button class="admin-btn ${b.primary ? 'admin-btn-primary' : ''}" onclick="(${b.onClick.toString()})()">${b.text}</button>
`).join('')}
</div>
` : ''}
</div>
`;
modal.classList.add('active');
}
function hideCustomModal() {
const modal = document.getElementById('custom-modal');
if (modal) modal.classList.remove('active');
}
// ==========================================
// COOKIE CATEGORIES MANAGEMENT
// ==========================================
async function loadAdminCookieCategories() {
const container = document.getElementById('admin-cookie-table-container');
container.innerHTML = '<div class="admin-loading">Lade Cookie-Kategorien...</div>';
try {
const res = await fetch('/api/consent/admin/cookies/categories');
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
adminCookieCategories = data.categories || [];
renderCookieCategoriesTable();
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Kategorien.</div>';
}
}
function renderCookieCategoriesTable() {
const container = document.getElementById('admin-cookie-table-container');
if (adminCookieCategories.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Cookie-Kategorien vorhanden.</div>';
return;
}
const html = `
<table class="admin-table">
<thead>
<tr>
<th>Name</th>
<th>Anzeigename (DE)</th>
<th>Typ</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${adminCookieCategories.map(cat => `
<tr>
<td><code>${cat.name}</code></td>
<td>${cat.display_name_de}</td>
<td>
${cat.is_mandatory ? '<span class="admin-badge admin-badge-mandatory">Notwendig</span>' : '<span class="admin-badge admin-badge-optional">Optional</span>'}
</td>
<td class="admin-actions">
<button class="admin-btn admin-btn-edit" onclick="editCookieCategory('${cat.id}')">Bearbeiten</button>
${!cat.is_mandatory ? `<button class="admin-btn admin-btn-delete" onclick="deleteCookieCategory('${cat.id}')">Löschen</button>` : ''}
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = html;
}
function showCookieForm(cat = null) {
const form = document.getElementById('admin-cookie-form');
if (cat) {
document.getElementById('admin-cookie-id').value = cat.id;
document.getElementById('admin-cookie-name').value = cat.name;
document.getElementById('admin-cookie-display-de').value = cat.display_name_de;
document.getElementById('admin-cookie-display-en').value = cat.display_name_en || '';
document.getElementById('admin-cookie-desc-de').value = cat.description_de || '';
document.getElementById('admin-cookie-mandatory').checked = cat.is_mandatory;
} else {
document.getElementById('admin-cookie-id').value = '';
document.getElementById('admin-cookie-name').value = '';
document.getElementById('admin-cookie-display-de').value = '';
document.getElementById('admin-cookie-display-en').value = '';
document.getElementById('admin-cookie-desc-de').value = '';
document.getElementById('admin-cookie-mandatory').checked = false;
}
form.classList.add('active');
}
function hideCookieForm() {
document.getElementById('admin-cookie-form').classList.remove('active');
}
function editCookieCategory(catId) {
const cat = adminCookieCategories.find(c => c.id === catId);
if (cat) showCookieForm(cat);
}
async function saveCookieCategory() {
const catId = document.getElementById('admin-cookie-id').value;
const data = {
name: document.getElementById('admin-cookie-name').value,
display_name_de: document.getElementById('admin-cookie-display-de').value,
display_name_en: document.getElementById('admin-cookie-display-en').value,
description_de: document.getElementById('admin-cookie-desc-de').value,
is_mandatory: document.getElementById('admin-cookie-mandatory').checked
};
try {
const url = catId ? `/api/consent/admin/cookies/categories/${catId}` : '/api/consent/admin/cookies/categories';
const method = catId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('Failed to save');
hideCookieForm();
await loadAdminCookieCategories();
alert('Kategorie gespeichert!');
} catch(e) {
alert('Fehler beim Speichern: ' + e.message);
}
}
async function deleteCookieCategory(catId) {
if (!confirm('Kategorie wirklich löschen?')) return;
try {
const res = await fetch(`/api/consent/admin/cookies/categories/${catId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete');
await loadAdminCookieCategories();
alert('Kategorie gelöscht!');
} catch(e) {
alert('Fehler: ' + e.message);
}
}
// ==========================================
// STATISTICS & GDPR EXPORT
// ==========================================
let dataCategories = [];
async function loadAdminStats() {
const container = document.getElementById('admin-stats-container');
container.innerHTML = '<div class="admin-loading">Lade Statistiken & DSGVO-Informationen...</div>';
try {
// Lade Datenkategorien
const catRes = await fetch('/api/consent/privacy/data-categories');
if (catRes.ok) {
const catData = await catRes.json();
dataCategories = catData.categories || [];
}
renderStatsPanel();
} catch(e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden: ' + e.message + '</div>';
}
}
function renderStatsPanel() {
const container = document.getElementById('admin-stats-container');
// Kategorisiere Daten
const essential = dataCategories.filter(c => c.is_essential);
const optional = dataCategories.filter(c => !c.is_essential);
const html = `
<div style="display: grid; gap: 24px;">
<!-- GDPR Export Section -->
<div class="admin-form" style="padding: 20px;">
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
<span style="margin-right: 8px;">📋</span> DSGVO-Datenauskunft (Art. 15)
</h3>
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 16px;">
Exportieren Sie alle personenbezogenen Daten eines Nutzers als PDF-Dokument.
Dies erfüllt die Anforderungen der DSGVO Art. 15 (Auskunftsrecht).
</p>
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<input type="text" id="gdpr-export-user-id" class="admin-form-input"
placeholder="Benutzer-ID (optional für eigene Daten)"
style="flex: 1; min-width: 200px;">
<button class="admin-btn admin-btn-primary" onclick="exportUserDataPdf()">
PDF exportieren
</button>
<button class="admin-btn" onclick="previewUserDataHtml()">
HTML-Vorschau
</button>
</div>
<div id="gdpr-export-status" style="margin-top: 12px; font-size: 13px;"></div>
</div>
<!-- Data Retention Overview -->
<div class="admin-form" style="padding: 20px;">
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
<span style="margin-right: 8px;">🗄️</span> Datenkategorien & Löschfristen
</h3>
<div style="margin-bottom: 20px;">
<h4 style="font-size: 14px; color: var(--bp-primary); margin: 0 0 12px 0;">
Essentielle Daten (Pflicht für Betrieb)
</h4>
<table class="admin-table">
<thead>
<tr>
<th>Kategorie</th>
<th>Beschreibung</th>
<th>Löschfrist</th>
<th>Rechtsgrundlage</th>
</tr>
</thead>
<tbody>
${essential.map(cat => `
<tr>
<td><strong>${cat.name_de}</strong></td>
<td>${cat.description_de}</td>
<td><span class="admin-badge admin-badge-published">${cat.retention_period}</span></td>
<td style="font-size: 11px; color: var(--bp-text-muted);">${cat.legal_basis}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div>
<h4 style="font-size: 14px; color: var(--bp-warning); margin: 0 0 12px 0;">
Optionale Daten (nur bei Einwilligung)
</h4>
<table class="admin-table">
<thead>
<tr>
<th>Kategorie</th>
<th>Beschreibung</th>
<th>Cookie-Kategorie</th>
<th>Löschfrist</th>
</tr>
</thead>
<tbody>
${optional.map(cat => `
<tr>
<td><strong>${cat.name_de}</strong></td>
<td>${cat.description_de}</td>
<td><span class="admin-badge">${cat.cookie_category || '-'}</span></td>
<td><span class="admin-badge admin-badge-optional">${cat.retention_period}</span></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
<!-- Quick Stats -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
<div class="admin-form" style="padding: 16px; text-align: center;">
<div style="font-size: 28px; font-weight: bold; color: var(--bp-primary);">${dataCategories.length}</div>
<div style="color: var(--bp-text-muted); font-size: 13px;">Datenkategorien</div>
</div>
<div class="admin-form" style="padding: 16px; text-align: center;">
<div style="font-size: 28px; font-weight: bold; color: var(--bp-success);">${essential.length}</div>
<div style="color: var(--bp-text-muted); font-size: 13px;">Essentiell</div>
</div>
<div class="admin-form" style="padding: 16px; text-align: center;">
<div style="font-size: 28px; font-weight: bold; color: var(--bp-warning);">${optional.length}</div>
<div style="color: var(--bp-text-muted); font-size: 13px;">Optional (Opt-in)</div>
</div>
</div>
</div>
`;
container.innerHTML = html;
}
async function exportUserDataPdf() {
const userIdInput = document.getElementById('gdpr-export-user-id');
const statusDiv = document.getElementById('gdpr-export-status');
const userId = userIdInput?.value?.trim();
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Generiere PDF...</span>';
try {
let url = '/api/consent/privacy/export-pdf';
// Wenn eine User-ID angegeben wurde, verwende den Admin-Endpoint
if (userId) {
url = `/api/consent/admin/privacy/export-pdf/${userId}`;
}
const res = await fetch(url, { method: 'POST' });
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail?.message || error.detail || 'Export fehlgeschlagen');
}
// PDF herunterladen
const blob = await res.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = userId ? `datenauskunft_${userId.slice(0,8)}.pdf` : 'breakpilot_datenauskunft.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ PDF erfolgreich generiert!</span>';
} catch(e) {
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
}
}
async function previewUserDataHtml() {
const statusDiv = document.getElementById('gdpr-export-status');
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Lade Vorschau...</span>';
try {
const res = await fetch('/api/consent/privacy/export-html');
if (!res.ok) {
throw new Error('Vorschau konnte nicht geladen werden');
}
const html = await res.text();
// In neuem Tab öffnen
const win = window.open('', '_blank');
win.document.write(html);
win.document.close();
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ Vorschau in neuem Tab geöffnet</span>';
} catch(e) {
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
}
}
// ==========================================
// DSR (DATA SUBJECT REQUESTS) FUNCTIONS
// ==========================================
let dsrList = [];
let currentDSR = null;
const DSR_TYPE_LABELS = {
'access': 'Art. 15 - Auskunft',
'rectification': 'Art. 16 - Berichtigung',
'erasure': 'Art. 17 - Löschung',
'restriction': 'Art. 18 - Einschränkung',
'portability': 'Art. 20 - Datenübertragbarkeit'
};
const DSR_STATUS_LABELS = {
'intake': 'Eingang',
'identity_verification': 'Identitätsprüfung',
'processing': 'In Bearbeitung',
'completed': 'Abgeschlossen',
'rejected': 'Abgelehnt',
'cancelled': 'Storniert'
};
const DSR_STATUS_COLORS = {
'intake': '#6366f1',
'identity_verification': '#f59e0b',
'processing': '#3b82f6',
'completed': '#22c55e',
'rejected': '#ef4444',
'cancelled': '#6b7280'
};
async function loadDSRStats() {
const container = document.getElementById('dsr-stats-cards');
try {
const res = await fetch('/api/v1/admin/dsr/stats');
if (!res.ok) throw new Error('Failed to load stats');
const stats = await res.json();
container.innerHTML = `
<div class="dsms-status-card" style="border-left: 3px solid #ef4444;">
<h4>Überfällig</h4>
<div class="value" style="color: #ef4444; font-size: 24px;">${stats.overdue_requests || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid #3b82f6;">
<h4>In Bearbeitung</h4>
<div class="value" style="color: #3b82f6; font-size: 24px;">${stats.pending_requests || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid #22c55e;">
<h4>Diesen Monat abgeschlossen</h4>
<div class="value" style="color: #22c55e; font-size: 24px;">${stats.completed_this_month || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid var(--bp-primary);">
<h4>Gesamt</h4>
<div class="value" style="font-size: 24px;">${stats.total_requests || 0}</div>
</div>
<div class="dsms-status-card" style="border-left: 3px solid var(--bp-text-muted);">
<h4>Ø Bearbeitungszeit</h4>
<div class="value" style="font-size: 18px;">${(stats.average_processing_days || 0).toFixed(1)} Tage</div>
</div>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">Fehler beim Laden der Statistiken: ${e.message}</div>`;
}
}
async function loadDSRList() {
const container = document.getElementById('dsr-table-container');
const status = document.getElementById('dsr-filter-status').value;
const requestType = document.getElementById('dsr-filter-type').value;
const overdueOnly = document.getElementById('dsr-filter-overdue').checked;
container.innerHTML = '<div class="admin-loading">Lade Betroffenenanfragen...</div>';
try {
let url = '/api/v1/admin/dsr?limit=50';
if (status) url += `&status=${status}`;
if (requestType) url += `&request_type=${requestType}`;
if (overdueOnly) url += `&overdue_only=true`;
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to load DSRs');
const data = await res.json();
dsrList = data.requests || [];
if (dsrList.length === 0) {
container.innerHTML = `
<div class="admin-empty">
<p style="font-size: 48px;">📋</p>
<p>Keine Betroffenenanfragen gefunden.</p>
</div>
`;
return;
}
container.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Nr.</th>
<th>Typ</th>
<th>Antragsteller</th>
<th>Status</th>
<th>Priorität</th>
<th>Frist</th>
<th>Erstellt</th>
<th></th>
</tr>
</thead>
<tbody>
${dsrList.map(dsr => {
const isOverdue = new Date(dsr.deadline_at) < new Date() && !['completed', 'rejected', 'cancelled'].includes(dsr.status);
const deadlineDate = new Date(dsr.deadline_at).toLocaleDateString('de-DE');
return `
<tr style="${isOverdue ? 'background: rgba(239, 68, 68, 0.1);' : ''}">
<td><strong>${dsr.request_number}</strong></td>
<td>${DSR_TYPE_LABELS[dsr.request_type] || dsr.request_type}</td>
<td>
<div>${dsr.requester_email}</div>
${dsr.requester_name ? `<div style="font-size: 11px; color: var(--bp-text-muted);">${dsr.requester_name}</div>` : ''}
</td>
<td>
<span style="background: ${DSR_STATUS_COLORS[dsr.status]}20; color: ${DSR_STATUS_COLORS[dsr.status]}; padding: 2px 8px; border-radius: 12px; font-size: 11px;">
${DSR_STATUS_LABELS[dsr.status] || dsr.status}
</span>
</td>
<td>
<span style="color: ${dsr.priority === 'high' ? '#f59e0b' : dsr.priority === 'expedited' ? '#ef4444' : 'var(--bp-text-muted)'};">
${dsr.priority === 'expedited' ? '🔴' : dsr.priority === 'high' ? '🟡' : ''}
${dsr.priority === 'expedited' ? 'Beschleunigt' : dsr.priority === 'high' ? 'Hoch' : 'Normal'}
</span>
</td>
<td style="${isOverdue ? 'color: #ef4444; font-weight: 600;' : ''}">${deadlineDate}${isOverdue ? ' ⚠️' : ''}</td>
<td>${new Date(dsr.created_at).toLocaleDateString('de-DE')}</td>
<td>
<button class="btn btn-ghost btn-sm" onclick="showDSRDetail('${dsr.id}')">Details</button>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<div style="margin-top: 12px; font-size: 12px; color: var(--bp-text-muted);">
${data.total || dsrList.length} Anfragen gefunden
</div>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
}
}
function showDSRCreateForm() {
document.getElementById('dsr-create-form').style.display = 'block';
document.getElementById('dsr-create-type').value = '';
document.getElementById('dsr-create-priority').value = 'normal';
document.getElementById('dsr-create-email').value = '';
document.getElementById('dsr-create-name').value = '';
document.getElementById('dsr-create-phone').value = '';
}
function hideDSRCreateForm() {
document.getElementById('dsr-create-form').style.display = 'none';
}
async function createDSR() {
const type = document.getElementById('dsr-create-type').value;
const priority = document.getElementById('dsr-create-priority').value;
const email = document.getElementById('dsr-create-email').value;
const name = document.getElementById('dsr-create-name').value;
const phone = document.getElementById('dsr-create-phone').value;
if (!type || !email) {
alert('Bitte füllen Sie alle Pflichtfelder aus.');
return;
}
try {
const res = await fetch('/api/v1/admin/dsr', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
request_type: type,
priority: priority,
requester_email: email,
requester_name: name || undefined,
requester_phone: phone || undefined,
source: 'admin_panel'
})
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || err.detail || 'Fehler beim Erstellen');
}
const data = await res.json();
alert(`Anfrage ${data.request_number} wurde erstellt.`);
hideDSRCreateForm();
loadDSRStats();
loadDSRList();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRDetail(dsrId) {
try {
const res = await fetch(`/api/v1/admin/dsr/${dsrId}`);
if (!res.ok) throw new Error('Failed to load DSR');
currentDSR = await res.json();
// Load history
const historyRes = await fetch(`/api/v1/admin/dsr/${dsrId}/history`);
const historyData = historyRes.ok ? await historyRes.json() : { history: [] };
document.getElementById('dsr-table-container').style.display = 'none';
document.getElementById('dsr-create-form').style.display = 'none';
document.getElementById('dsr-detail-view').style.display = 'block';
const isOverdue = new Date(currentDSR.deadline_at) < new Date() && !['completed', 'rejected', 'cancelled'].includes(currentDSR.status);
document.getElementById('dsr-detail-content').innerHTML = `
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px;">
<div>
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h3 style="margin: 0 0 16px 0; display: flex; align-items: center; gap: 12px;">
${currentDSR.request_number}
<span style="background: ${DSR_STATUS_COLORS[currentDSR.status]}20; color: ${DSR_STATUS_COLORS[currentDSR.status]}; padding: 4px 12px; border-radius: 12px; font-size: 12px;">
${DSR_STATUS_LABELS[currentDSR.status] || currentDSR.status}
</span>
</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Anfragetyp</div>
<div style="font-weight: 500;">${DSR_TYPE_LABELS[currentDSR.request_type] || currentDSR.request_type}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Priorität</div>
<div style="font-weight: 500;">${currentDSR.priority === 'expedited' ? '🔴 Beschleunigt' : currentDSR.priority === 'high' ? '🟡 Hoch' : 'Normal'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Frist</div>
<div style="font-weight: 500; ${isOverdue ? 'color: #ef4444;' : ''}">${new Date(currentDSR.deadline_at).toLocaleDateString('de-DE')} ${isOverdue ? '⚠️ ÜBERFÄLLIG' : ''}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Gesetzliche Frist</div>
<div style="font-weight: 500;">${currentDSR.legal_deadline_days} Tage</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Identität verifiziert</div>
<div style="font-weight: 500;">${currentDSR.identity_verified ? '✅ Ja' : '❌ Nein'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Quelle</div>
<div style="font-weight: 500;">${currentDSR.source === 'api' ? 'API' : currentDSR.source === 'admin_panel' ? 'Admin Panel' : currentDSR.source}</div>
</div>
</div>
</div>
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0;">Antragsteller</h4>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">E-Mail</div>
<div style="font-weight: 500;">${currentDSR.requester_email}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Name</div>
<div style="font-weight: 500;">${currentDSR.requester_name || '-'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--bp-text-muted);">Telefon</div>
<div style="font-weight: 500;">${currentDSR.requester_phone || '-'}</div>
</div>
</div>
</div>
${currentDSR.processing_notes ? `
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0;">Bearbeitungsnotizen</h4>
<div style="white-space: pre-wrap;">${currentDSR.processing_notes}</div>
</div>
` : ''}
${currentDSR.result_summary ? `
<div style="background: rgba(34, 197, 94, 0.1); border: 1px solid #22c55e; border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0; color: #22c55e;">Ergebnis</h4>
<div>${currentDSR.result_summary}</div>
</div>
` : ''}
${currentDSR.rejection_reason ? `
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid #ef4444; border-radius: 8px; padding: 20px; margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0; color: #ef4444;">Ablehnung</h4>
<div><strong>Rechtsgrundlage:</strong> ${currentDSR.rejection_legal_basis}</div>
<div style="margin-top: 8px;">${currentDSR.rejection_reason}</div>
</div>
` : ''}
</div>
<div>
<div style="background: var(--bp-surface); border: 1px solid var(--bp-border); border-radius: 8px; padding: 20px;">
<h4 style="margin: 0 0 12px 0;">Verlauf</h4>
<div style="max-height: 400px; overflow-y: auto;">
${(historyData.history || []).map(h => `
<div style="padding: 8px 0; border-bottom: 1px solid var(--bp-border);">
<div style="font-size: 11px; color: var(--bp-text-muted);">${new Date(h.created_at).toLocaleString('de-DE')}</div>
<div style="font-size: 13px;">
${h.from_status ? `${DSR_STATUS_LABELS[h.from_status] || h.from_status} → ` : ''}
<strong>${DSR_STATUS_LABELS[h.to_status] || h.to_status}</strong>
</div>
${h.comment ? `<div style="font-size: 12px; color: var(--bp-text-muted); margin-top: 4px;">${h.comment}</div>` : ''}
</div>
`).join('') || '<div style="color: var(--bp-text-muted);">Kein Verlauf vorhanden</div>'}
</div>
</div>
</div>
</div>
`;
// Update button visibility based on status
const canVerify = !currentDSR.identity_verified && ['intake', 'identity_verification'].includes(currentDSR.status);
const canComplete = ['processing'].includes(currentDSR.status);
const canReject = ['intake', 'identity_verification', 'processing'].includes(currentDSR.status);
const canExtend = !['completed', 'rejected', 'cancelled'].includes(currentDSR.status);
document.getElementById('dsr-btn-verify').style.display = canVerify ? 'inline-flex' : 'none';
document.getElementById('dsr-btn-complete').style.display = canComplete ? 'inline-flex' : 'none';
document.getElementById('dsr-btn-reject').style.display = canReject ? 'inline-flex' : 'none';
document.getElementById('dsr-btn-extend').style.display = canExtend ? 'inline-flex' : 'none';
} catch(e) {
alert('Fehler beim Laden: ' + e.message);
}
}
function hideDSRDetail() {
document.getElementById('dsr-detail-view').style.display = 'none';
document.getElementById('dsr-table-container').style.display = 'block';
currentDSR = null;
}
async function verifyDSRIdentity() {
if (!currentDSR) return;
const method = prompt('Verifizierungsmethode (z.B. id_card, passport, video_call, email):', 'email');
if (!method) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/verify-identity`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method: method })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Identität wurde verifiziert.');
showDSRDetail(currentDSR.id);
loadDSRStats();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRExtendDialog() {
if (!currentDSR) return;
const reason = prompt('Begründung für die Fristverlängerung:');
if (!reason) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/extend`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason, days: 60 })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Frist wurde um 60 Tage verlängert.');
showDSRDetail(currentDSR.id);
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRCompleteDialog() {
if (!currentDSR) return;
const summary = prompt('Zusammenfassung des Ergebnisses:');
if (!summary) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ summary: summary })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Anfrage wurde abgeschlossen.');
showDSRDetail(currentDSR.id);
loadDSRStats();
loadDSRList();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function showDSRRejectDialog() {
if (!currentDSR) return;
const legalBasis = prompt('Rechtsgrundlage für die Ablehnung (z.B. Art. 17(3)a, Art. 12(5)):');
if (!legalBasis) return;
const reason = prompt('Begründung der Ablehnung:');
if (!reason) return;
try {
const res = await fetch(`/api/v1/admin/dsr/${currentDSR.id}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason, legal_basis: legalBasis })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail?.error || 'Fehler');
}
alert('Anfrage wurde abgelehnt.');
showDSRDetail(currentDSR.id);
loadDSRStats();
loadDSRList();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
function showDSRAssignDialog() {
// TODO: Implement user selection dialog
alert('Zuweisung noch nicht implementiert. Verwenden Sie die API direkt.');
}
function loadDSRData() {
loadDSRStats();
loadDSRList();
}
// Load DSR data when tab is clicked
document.querySelector('.admin-tab[data-tab="dsr"]')?.addEventListener('click', loadDSRData);
// ==========================================
// DSMS FUNCTIONS
// ==========================================
const DSMS_GATEWAY_URL = 'http://localhost:8082';
let dsmsArchives = [];
function switchDsmsTab(tabName) {
document.querySelectorAll('.dsms-subtab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.dsms-content').forEach(c => c.classList.remove('active'));
document.querySelector(`.dsms-subtab[data-dsms-tab="${tabName}"]`)?.classList.add('active');
document.getElementById(`dsms-${tabName}`)?.classList.add('active');
// Load data for specific tabs
if (tabName === 'settings') {
loadDsmsNodeInfo();
}
}
async function loadDsmsData() {
await Promise.all([
loadDsmsStatus(),
loadDsmsArchives(),
loadDsmsDocumentSelect()
]);
}
async function loadDsmsStatus() {
const container = document.getElementById('dsms-status-cards');
container.innerHTML = '<div class="admin-loading">Lade DSMS Status...</div>';
try {
const [healthRes, nodeRes] = await Promise.all([
fetch(`${DSMS_GATEWAY_URL}/health`).catch(() => null),
fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`).catch(() => null)
]);
const health = healthRes?.ok ? await healthRes.json() : null;
const nodeInfo = nodeRes?.ok ? await nodeRes.json() : null;
const isOnline = health?.ipfs_connected === true;
const repoSize = nodeInfo?.repo_size ? formatBytes(nodeInfo.repo_size) : '-';
const storageMax = nodeInfo?.storage_max ? formatBytes(nodeInfo.storage_max) : '-';
const numObjects = nodeInfo?.num_objects ?? '-';
container.innerHTML = `
<div class="dsms-status-card">
<h4>Status</h4>
<div class="value ${isOnline ? 'online' : 'offline'}">${isOnline ? 'Online' : 'Offline'}</div>
</div>
<div class="dsms-status-card">
<h4>Speicher verwendet</h4>
<div class="value">${repoSize}</div>
</div>
<div class="dsms-status-card">
<h4>Max. Speicher</h4>
<div class="value">${storageMax}</div>
</div>
<div class="dsms-status-card">
<h4>Objekte</h4>
<div class="value">${numObjects}</div>
</div>
`;
} catch(e) {
container.innerHTML = `
<div class="dsms-status-card" style="grid-column: 1/-1;">
<h4>Status</h4>
<div class="value offline">Nicht erreichbar</div>
<p style="font-size: 12px; color: var(--bp-text-muted); margin-top: 8px;">
DSMS Gateway ist nicht verfügbar. Stellen Sie sicher, dass die Container laufen.
</p>
</div>
`;
}
}
async function loadDsmsArchives() {
const container = document.getElementById('dsms-archives-table');
container.innerHTML = '<div class="admin-loading">Lade archivierte Dokumente...</div>';
try {
const token = localStorage.getItem('bp_token') || '';
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/documents`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) {
throw new Error('Fehler beim Laden');
}
const data = await res.json();
dsmsArchives = data.documents || [];
if (dsmsArchives.length === 0) {
container.innerHTML = `
<div class="admin-empty">
<p>Keine archivierten Dokumente vorhanden.</p>
<p style="font-size: 12px; color: var(--bp-text-muted);">
Klicken Sie auf "+ Dokument archivieren" um ein Legal Document im DSMS zu speichern.
</p>
</div>
`;
return;
}
container.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>CID</th>
<th>Dokument</th>
<th>Version</th>
<th>Archiviert am</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
${dsmsArchives.map(doc => `
<tr>
<td>
<code style="font-size: 11px; background: var(--bp-surface-elevated); padding: 2px 6px; border-radius: 4px;">
${doc.cid.substring(0, 12)}...
</code>
</td>
<td>${doc.metadata?.document_id || doc.filename || '-'}</td>
<td>${doc.metadata?.version || '-'}</td>
<td>${doc.metadata?.created_at ? new Date(doc.metadata.created_at).toLocaleString('de-DE') : '-'}</td>
<td>
<button class="btn btn-ghost btn-sm" onclick="verifyDsmsDocumentByCid('${doc.cid}')" title="Verifizieren">
</button>
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${doc.cid}')" title="CID kopieren">
📋
</button>
<a href="${DSMS_GATEWAY_URL.replace('8082', '8085')}/ipfs/${doc.cid}" target="_blank" class="btn btn-ghost btn-sm" title="Im Gateway öffnen">
</a>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
}
}
async function loadDsmsDocumentSelect() {
const select = document.getElementById('dsms-archive-doc-select');
if (!select) return;
try {
const res = await fetch('/api/consent/admin/documents');
if (!res.ok) return;
const data = await res.json();
const docs = data.documents || [];
select.innerHTML = '<option value="">-- Dokument wählen --</option>' +
docs.map(d => `<option value="${d.id}">${d.name} (${d.type})</option>`).join('');
} catch(e) {
console.error('Error loading documents:', e);
}
}
async function loadDsmsVersionSelect() {
const docSelect = document.getElementById('dsms-archive-doc-select');
const versionSelect = document.getElementById('dsms-archive-version-select');
const docId = docSelect?.value;
if (!docId) {
versionSelect.innerHTML = '<option value="">-- Erst Dokument wählen --</option>';
versionSelect.disabled = true;
return;
}
versionSelect.disabled = false;
versionSelect.innerHTML = '<option value="">Lade Versionen...</option>';
try {
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
if (!res.ok) throw new Error('Fehler');
const data = await res.json();
const versions = data.versions || [];
if (versions.length === 0) {
versionSelect.innerHTML = '<option value="">Keine Versionen vorhanden</option>';
return;
}
versionSelect.innerHTML = '<option value="">-- Version wählen --</option>' +
versions.map(v => `<option value="${v.id}" data-content="${encodeURIComponent(v.content || '')}" data-version="${v.version}">
${v.version} (${v.language}) - ${v.status}
</option>`).join('');
} catch(e) {
versionSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
}
}
// Attach event listener for doc select change
document.getElementById('dsms-archive-doc-select')?.addEventListener('change', loadDsmsVersionSelect);
function showArchiveForm() {
document.getElementById('dsms-archive-form').style.display = 'block';
loadDsmsDocumentSelect();
}
function hideArchiveForm() {
document.getElementById('dsms-archive-form').style.display = 'none';
}
async function archiveDocumentToDsms() {
const docSelect = document.getElementById('dsms-archive-doc-select');
const versionSelect = document.getElementById('dsms-archive-version-select');
const selectedOption = versionSelect.options[versionSelect.selectedIndex];
if (!docSelect.value || !versionSelect.value) {
alert('Bitte Dokument und Version auswählen');
return;
}
const content = decodeURIComponent(selectedOption.dataset.content || '');
const version = selectedOption.dataset.version;
const docId = docSelect.value;
if (!content) {
alert('Die ausgewählte Version hat keinen Inhalt');
return;
}
try {
const token = localStorage.getItem('bp_token') || '';
const params = new URLSearchParams({
document_id: docId,
version: version,
content: content,
language: 'de'
});
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/legal-documents/archive?${params}`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Archivierung fehlgeschlagen');
}
const result = await res.json();
alert(`Dokument erfolgreich archiviert!\\n\\nCID: ${result.cid}\\nChecksum: ${result.checksum}`);
hideArchiveForm();
loadDsmsArchives();
} catch(e) {
alert('Fehler: ' + e.message);
}
}
async function verifyDsmsDocument() {
const cidInput = document.getElementById('dsms-verify-cid');
const resultDiv = document.getElementById('dsms-verify-result');
const cid = cidInput?.value?.trim();
if (!cid) {
alert('Bitte CID eingeben');
return;
}
await verifyDsmsDocumentByCid(cid);
}
async function verifyDsmsDocumentByCid(cid) {
const resultDiv = document.getElementById('dsms-verify-result');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="admin-loading">Verifiziere...</div>';
// Switch to verify tab
switchDsmsTab('verify');
document.getElementById('dsms-verify-cid').value = cid;
try {
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/verify/${cid}`);
const data = await res.json();
if (data.exists && data.integrity_valid) {
resultDiv.innerHTML = `
<div class="dsms-verify-success">
<h4 style="margin: 0 0 12px 0;">✓ Dokument verifiziert</h4>
<div style="display: grid; gap: 8px; font-size: 13px;">
<div><strong>CID:</strong> <code>${cid}</code></div>
<div><strong>Integrität:</strong> Gültig</div>
<div><strong>Typ:</strong> ${data.metadata?.document_type || '-'}</div>
<div><strong>Dokument-ID:</strong> ${data.metadata?.document_id || '-'}</div>
<div><strong>Version:</strong> ${data.metadata?.version || '-'}</div>
<div><strong>Erstellt:</strong> ${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '-'}</div>
<div><strong>Checksum:</strong> <code style="font-size: 10px;">${data.stored_checksum || '-'}</code></div>
</div>
</div>
`;
} else if (data.exists && !data.integrity_valid) {
resultDiv.innerHTML = `
<div class="dsms-verify-error">
<h4 style="margin: 0 0 12px 0;">⚠ Integritätsfehler</h4>
<p>Das Dokument existiert, aber die Prüfsumme stimmt nicht überein.</p>
<p style="font-size: 12px;">Gespeichert: ${data.stored_checksum}</p>
<p style="font-size: 12px;">Berechnet: ${data.calculated_checksum}</p>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="dsms-verify-error">
<h4 style="margin: 0 0 12px 0;">✗ Nicht gefunden</h4>
<p>Kein Dokument mit diesem CID gefunden.</p>
${data.error ? `<p style="font-size: 12px;">${data.error}</p>` : ''}
</div>
`;
}
} catch(e) {
resultDiv.innerHTML = `
<div class="dsms-verify-error">
<h4 style="margin: 0 0 12px 0;">Fehler</h4>
<p>${e.message}</p>
</div>
`;
}
}
async function loadDsmsNodeInfo() {
const container = document.getElementById('dsms-node-info');
container.innerHTML = '<div class="admin-loading">Lade Node-Info...</div>';
try {
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`);
if (!res.ok) throw new Error('Nicht erreichbar');
const info = await res.json();
container.innerHTML = `
<div style="display: grid; gap: 12px; font-size: 13px;">
<div><strong>Node ID:</strong> <code style="font-size: 11px;">${info.node_id || '-'}</code></div>
<div><strong>Agent:</strong> ${info.agent_version || '-'}</div>
<div><strong>Repo-Größe:</strong> ${info.repo_size ? formatBytes(info.repo_size) : '-'}</div>
<div><strong>Max. Speicher:</strong> ${info.storage_max ? formatBytes(info.storage_max) : '-'}</div>
<div><strong>Objekte:</strong> ${info.num_objects ?? '-'}</div>
<div>
<strong>Adressen:</strong>
<ul style="margin: 4px 0 0 0; padding-left: 20px; font-size: 11px; color: var(--bp-text-muted);">
${(info.addresses || []).map(a => `<li><code>${a}</code></li>`).join('')}
</ul>
</div>
</div>
`;
} catch(e) {
container.innerHTML = `<div class="admin-empty">DSMS nicht erreichbar: ${e.message}</div>`;
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Optional: Show toast
}).catch(err => {
console.error('Copy failed:', err);
});
}
// ==========================================
// DSMS WEBUI FUNCTIONS
// ==========================================
function openDsmsWebUI() {
document.getElementById('dsms-webui-modal').style.display = 'flex';
loadDsmsWebUIData();
}
function closeDsmsWebUI() {
document.getElementById('dsms-webui-modal').style.display = 'none';
}
function switchDsmsWebUISection(section) {
// Update nav buttons
document.querySelectorAll('.dsms-webui-nav').forEach(btn => {
btn.classList.toggle('active', btn.dataset.section === section);
});
// Update sections
document.querySelectorAll('.dsms-webui-section').forEach(sec => {
sec.classList.remove('active');
sec.style.display = 'none';
});
const activeSection = document.getElementById('dsms-webui-' + section);
if (activeSection) {
activeSection.classList.add('active');
activeSection.style.display = 'block';
}
// Load section-specific data
if (section === 'peers') loadDsmsPeers();
}
async function loadDsmsWebUIData() {
try {
// Load node info
const infoRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/node/info');
const info = await infoRes.json();
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-accent);">Online</span>';
document.getElementById('webui-node-id').textContent = info.node_id || '--';
document.getElementById('webui-protocol').textContent = info.protocol_version || '--';
document.getElementById('webui-agent').textContent = info.agent_version || '--';
document.getElementById('webui-repo-size').textContent = formatBytes(info.repo_size || 0);
document.getElementById('webui-storage-info').textContent = 'Max: ' + formatBytes(info.storage_max || 0);
document.getElementById('webui-num-objects').textContent = (info.num_objects || 0).toLocaleString();
// Addresses
const addresses = info.addresses || [];
document.getElementById('webui-addresses').innerHTML = addresses.length > 0
? addresses.map(a => '<div style="margin-bottom: 4px;">' + a + '</div>').join('')
: '<span style="color: var(--bp-text-muted);">Keine Adressen verfügbar</span>';
// Load pinned count
const token = localStorage.getItem('bp_token') || '';
const docsRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (docsRes.ok) {
const docs = await docsRes.json();
document.getElementById('webui-pinned-count').textContent = docs.total || 0;
}
} catch (e) {
console.error('Failed to load WebUI data:', e);
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-danger);">Offline</span>';
}
}
async function loadDsmsPeers() {
const container = document.getElementById('webui-peers-list');
try {
// IPFS peers endpoint via proxy would need direct IPFS API access
// For now, show info that private network has no peers
container.innerHTML = `
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; text-align: center;">
<div style="font-size: 48px; margin-bottom: 16px;">&#128274;</div>
<h4 style="margin: 0 0 8px 0;">Privates Netzwerk</h4>
<p style="color: var(--bp-text-muted); margin: 0;">
DSMS läuft als isolierter Node. Keine externen Peers verbunden.
</p>
</div>
`;
} catch (e) {
container.innerHTML = '<div class="dsms-verify-error">Fehler beim Laden der Peers</div>';
}
}
// File upload handlers
function handleDsmsDragOver(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('dsms-upload-zone').classList.add('dragover');
}
function handleDsmsDragLeave(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('dsms-upload-zone').classList.remove('dragover');
}
async function handleDsmsFileDrop(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('dsms-upload-zone').classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
await uploadDsmsFiles(files);
}
}
async function handleDsmsFileSelect(e) {
const files = e.target.files;
if (files.length > 0) {
await uploadDsmsFiles(files);
}
}
async function uploadDsmsFiles(files) {
const token = localStorage.getItem('bp_token') || '';
const progressDiv = document.getElementById('dsms-upload-progress');
const statusDiv = document.getElementById('dsms-upload-status');
const barDiv = document.getElementById('dsms-upload-bar');
const resultsDiv = document.getElementById('dsms-upload-results');
progressDiv.style.display = 'block';
resultsDiv.innerHTML = '';
const results = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
statusDiv.textContent = 'Lade hoch: ' + file.name + ' (' + (i+1) + '/' + files.length + ')';
barDiv.style.width = ((i / files.length) * 100) + '%';
try {
const formData = new FormData();
formData.append('file', file);
formData.append('document_type', 'legal_document');
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: formData
});
if (res.ok) {
const data = await res.json();
results.push({ file: file.name, cid: data.cid, success: true });
} else {
results.push({ file: file.name, error: 'Upload fehlgeschlagen', success: false });
}
} catch (e) {
results.push({ file: file.name, error: e.message, success: false });
}
}
barDiv.style.width = '100%';
statusDiv.textContent = 'Upload abgeschlossen!';
// Show results
resultsDiv.innerHTML = '<h4 style="margin: 0 0 12px 0;">Ergebnisse</h4>' +
results.map(r => `
<div class="dsms-webui-file-item">
<div>
<div style="font-weight: 500;">${r.file}</div>
${r.success
? '<div class="cid" style="color: var(--bp-accent);">CID: ' + r.cid + '</div>'
: '<div style="color: var(--bp-danger);">' + r.error + '</div>'
}
</div>
${r.success ? '<button class="btn btn-ghost btn-sm" onclick="copyToClipboard(\\''+r.cid+'\\')">Kopieren</button>' : ''}
</div>
`).join('');
setTimeout(() => {
progressDiv.style.display = 'none';
barDiv.style.width = '0%';
}, 2000);
}
async function exploreDsmsCid() {
const cid = document.getElementById('webui-explore-cid').value.trim();
if (!cid) return;
const resultDiv = document.getElementById('dsms-explore-result');
const contentDiv = document.getElementById('dsms-explore-content');
resultDiv.style.display = 'block';
contentDiv.innerHTML = '<div class="admin-loading">Lade...</div>';
try {
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/verify/' + cid);
const data = await res.json();
if (data.exists) {
contentDiv.innerHTML = `
<div style="margin-bottom: 16px;">
<span style="font-size: 24px; ${data.integrity_valid ? 'color: var(--bp-accent);' : 'color: var(--bp-danger);'}">
${data.integrity_valid ? '&#10003;' : '&#10007;'}
</span>
<span style="font-weight: 600; margin-left: 8px;">
${data.integrity_valid ? 'Dokument verifiziert' : 'Integritätsfehler'}
</span>
</div>
<table style="width: 100%; font-size: 13px;">
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted); width: 150px;">CID</td>
<td style="padding: 8px 0; font-family: monospace; word-break: break-all;">${cid}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">Typ</td>
<td style="padding: 8px 0;">${data.metadata?.document_type || '--'}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">Erstellt</td>
<td style="padding: 8px 0;">${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '--'}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: var(--bp-text-muted);">Checksum</td>
<td style="padding: 8px 0; font-family: monospace; font-size: 11px; word-break: break-all;">${data.stored_checksum || '--'}</td>
</tr>
</table>
<div style="margin-top: 16px;">
<a href="http://localhost:8085/ipfs/${cid}" target="_blank" class="btn btn-ghost btn-sm">Im Gateway öffnen</a>
</div>
`;
} else {
contentDiv.innerHTML = `
<div class="dsms-verify-error">
<strong>Nicht gefunden</strong><br>
CID existiert nicht im DSMS: ${cid}
</div>
`;
}
} catch (e) {
contentDiv.innerHTML = `
<div class="dsms-verify-error">
<strong>Fehler</strong><br>
${e.message}
</div>
`;
}
}
async function runDsmsGarbageCollection() {
if (!confirm('Garbage Collection ausführen? Dies entfernt nicht gepinnte Objekte.')) return;
try {
// Note: Direct GC requires IPFS API access - show info for now
alert('Garbage Collection wird im Hintergrund ausgeführt. Dies kann einige Minuten dauern.');
} catch (e) {
alert('Fehler: ' + e.message);
}
}
// Close modal on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeDsmsWebUI();
}
});
// Close modal on backdrop click
document.getElementById('dsms-webui-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'dsms-webui-modal') {
closeDsmsWebUI();
}
});
// Load DSMS data when tab is clicked
document.querySelector('.admin-tab[data-tab="dsms"]')?.addEventListener('click', loadDsmsData);
// ==========================================
// E-MAIL TEMPLATE MANAGEMENT
// ==========================================
let emailTemplates = [];
let emailTemplateVersions = [];
let currentEmailTemplateId = null;
let currentEmailVersionId = null;
// E-Mail-Template-Typen mit deutschen Namen
const emailTypeNames = {
'welcome': 'Willkommens-E-Mail',
'email_verification': 'E-Mail-Verifizierung',
'password_reset': 'Passwort zurücksetzen',
'password_changed': 'Passwort geändert',
'2fa_enabled': '2FA aktiviert',
'2fa_disabled': '2FA deaktiviert',
'new_device_login': 'Neues Gerät Login',
'suspicious_activity': 'Verdächtige Aktivität',
'account_locked': 'Account gesperrt',
'account_unlocked': 'Account entsperrt',
'deletion_requested': 'Löschung angefordert',
'deletion_confirmed': 'Löschung bestätigt',
'data_export_ready': 'Datenexport bereit',
'email_changed': 'E-Mail geändert',
'new_version_published': 'Neue Version veröffentlicht',
'consent_reminder': 'Consent Erinnerung',
'consent_deadline_warning': 'Consent Frist Warnung',
'account_suspended': 'Account suspendiert'
};
// Load E-Mail Templates when tab is clicked
document.querySelector('.admin-tab[data-tab="emails"]')?.addEventListener('click', loadEmailTemplates);
async function loadEmailTemplates() {
try {
const res = await fetch('/api/consent/admin/email-templates');
if (!res.ok) throw new Error('Fehler beim Laden der Templates');
const data = await res.json();
emailTemplates = data.templates || [];
populateEmailTemplateSelect();
} catch (e) {
console.error('Error loading email templates:', e);
showToast('Fehler beim Laden der E-Mail-Templates', 'error');
}
}
function populateEmailTemplateSelect() {
const select = document.getElementById('email-template-select');
select.innerHTML = '<option value="">-- E-Mail-Vorlage auswählen --</option>';
emailTemplates.forEach(item => {
const template = item.template; // API liefert verschachtelte Struktur
const opt = document.createElement('option');
opt.value = template.id;
opt.textContent = emailTypeNames[template.type] || template.name;
select.appendChild(opt);
});
}
async function loadEmailTemplateVersions() {
const select = document.getElementById('email-template-select');
const templateId = select.value;
const newVersionBtn = document.getElementById('btn-new-email-version');
const infoCard = document.getElementById('email-template-info');
const container = document.getElementById('email-version-table-container');
if (!templateId) {
newVersionBtn.disabled = true;
infoCard.style.display = 'none';
container.innerHTML = '<div class="admin-empty">Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.</div>';
currentEmailTemplateId = null;
return;
}
currentEmailTemplateId = templateId;
newVersionBtn.disabled = false;
// Finde das Template (API liefert verschachtelte Struktur)
const templateItem = emailTemplates.find(t => t.template.id === templateId);
const template = templateItem?.template;
if (template) {
infoCard.style.display = 'block';
document.getElementById('email-template-name').textContent = emailTypeNames[template.type] || template.name;
document.getElementById('email-template-description').textContent = template.description || 'Keine Beschreibung';
document.getElementById('email-template-type-badge').textContent = template.type;
// Variablen anzeigen (wird aus dem Default-Inhalt ermittelt)
try {
const defaultRes = await fetch(`/api/consent/admin/email-templates/default/${template.type}`);
if (defaultRes.ok) {
const defaultData = await defaultRes.json();
const variables = extractVariables(defaultData.body_html || '');
document.getElementById('email-template-variables').textContent = variables.join(', ') || 'Keine';
}
} catch (e) {
document.getElementById('email-template-variables').textContent = '-';
}
}
// Lade Versionen
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
try {
const res = await fetch(`/api/consent/admin/email-templates/${templateId}/versions`);
if (!res.ok) throw new Error('Fehler beim Laden');
const data = await res.json();
emailTemplateVersions = data.versions || [];
renderEmailVersionsTable();
} catch (e) {
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
}
}
function extractVariables(content) {
const matches = content.match(/\\{\\{([^}]+)\\}\\}/g) || [];
return [...new Set(matches.map(m => m.replace(/[{}]/g, '')))];
}
function renderEmailVersionsTable() {
const container = document.getElementById('email-version-table-container');
if (emailTemplateVersions.length === 0) {
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden. Erstellen Sie eine neue Version.</div>';
return;
}
const statusColors = {
'draft': 'draft',
'review': 'review',
'approved': 'approved',
'published': 'published',
'archived': 'archived'
};
const statusNames = {
'draft': 'Entwurf',
'review': 'In Prüfung',
'approved': 'Genehmigt',
'published': 'Veröffentlicht',
'archived': 'Archiviert'
};
container.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Version</th>
<th>Sprache</th>
<th>Betreff</th>
<th>Status</th>
<th>Aktualisiert</th>
<th style="text-align: right;">Aktionen</th>
</tr>
</thead>
<tbody>
${emailTemplateVersions.map(v => `
<tr>
<td><strong>${v.version}</strong></td>
<td>${v.language === 'de' ? '🇩🇪 DE' : '🇬🇧 EN'}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${v.subject}</td>
<td><span class="admin-badge badge-${statusColors[v.status]}">${statusNames[v.status] || v.status}</span></td>
<td>${new Date(v.updated_at).toLocaleDateString('de-DE')}</td>
<td style="text-align: right;">
<button class="btn btn-ghost btn-xs" onclick="previewEmailVersionById('${v.id}')" title="Vorschau">👁️</button>
${v.status === 'draft' ? `
<button class="btn btn-ghost btn-xs" onclick="editEmailVersion('${v.id}')" title="Bearbeiten">✏️</button>
<button class="btn btn-ghost btn-xs" onclick="submitEmailForReview('${v.id}')" title="Zur Prüfung">📤</button>
<button class="btn btn-ghost btn-xs" onclick="deleteEmailVersion('${v.id}')" title="Löschen">🗑️</button>
` : ''}
${v.status === 'review' ? `
<button class="btn btn-ghost btn-xs" onclick="showEmailApprovalDialogFor('${v.id}')" title="Genehmigen">✅</button>
<button class="btn btn-ghost btn-xs" onclick="rejectEmailVersion('${v.id}')" title="Ablehnen">❌</button>
` : ''}
${v.status === 'approved' ? `
<button class="btn btn-primary btn-xs" onclick="publishEmailVersion('${v.id}')" title="Veröffentlichen">🚀</button>
` : ''}
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function showEmailVersionForm() {
document.getElementById('email-version-form').style.display = 'block';
document.getElementById('email-version-form-title').textContent = 'Neue E-Mail-Version erstellen';
document.getElementById('email-version-id').value = '';
document.getElementById('email-version-number').value = '';
document.getElementById('email-version-subject').value = '';
document.getElementById('email-version-editor').innerHTML = '';
document.getElementById('email-version-text').value = '';
// Lade Default-Inhalt (API liefert verschachtelte Struktur)
const templateItem = emailTemplates.find(t => t.template.id === currentEmailTemplateId);
if (templateItem?.template) {
loadDefaultEmailContent(templateItem.template.type);
}
}
async function loadDefaultEmailContent(templateType) {
try {
const res = await fetch(`/api/consent/admin/email-templates/default/${templateType}`);
if (res.ok) {
const data = await res.json();
document.getElementById('email-version-subject').value = data.subject || '';
document.getElementById('email-version-editor').innerHTML = data.body_html || '';
document.getElementById('email-version-text').value = data.body_text || '';
}
} catch (e) {
console.error('Error loading default content:', e);
}
}
function hideEmailVersionForm() {
document.getElementById('email-version-form').style.display = 'none';
}
async function saveEmailVersion() {
const versionId = document.getElementById('email-version-id').value;
const templateId = currentEmailTemplateId;
const version = document.getElementById('email-version-number').value.trim();
const language = document.getElementById('email-version-lang').value;
const subject = document.getElementById('email-version-subject').value.trim();
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
const bodyText = document.getElementById('email-version-text').value.trim();
if (!version || !subject || !bodyHtml) {
showToast('Bitte füllen Sie alle Pflichtfelder aus', 'error');
return;
}
const data = {
template_id: templateId,
version: version,
language: language,
subject: subject,
body_html: bodyHtml,
body_text: bodyText || stripHtml(bodyHtml)
};
try {
let res;
if (versionId) {
// Update existing version
res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} else {
// Create new version
res = await fetch('/api/consent/admin/email-template-versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || 'Fehler beim Speichern');
}
showToast('E-Mail-Version gespeichert!', 'success');
hideEmailVersionForm();
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
}
function stripHtml(html) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
}
async function editEmailVersion(versionId) {
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`);
if (!res.ok) throw new Error('Version nicht gefunden');
const version = await res.json();
document.getElementById('email-version-form').style.display = 'block';
document.getElementById('email-version-form-title').textContent = 'E-Mail-Version bearbeiten';
document.getElementById('email-version-id').value = versionId;
document.getElementById('email-version-number').value = version.version;
document.getElementById('email-version-lang').value = version.language;
document.getElementById('email-version-subject').value = version.subject;
document.getElementById('email-version-editor').innerHTML = version.body_html;
document.getElementById('email-version-text').value = version.body_text || '';
} catch (e) {
showToast('Fehler beim Laden der Version', 'error');
}
}
async function deleteEmailVersion(versionId) {
if (!confirm('Möchten Sie diese Version wirklich löschen?')) return;
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
method: 'DELETE'
});
if (!res.ok) throw new Error('Fehler beim Löschen');
showToast('Version gelöscht', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler beim Löschen', 'error');
}
}
async function submitEmailForReview(versionId) {
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/submit`, {
method: 'POST'
});
if (!res.ok) throw new Error('Fehler');
showToast('Zur Prüfung eingereicht', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler beim Einreichen', 'error');
}
}
function showEmailApprovalDialogFor(versionId) {
currentEmailVersionId = versionId;
document.getElementById('email-approval-dialog').style.display = 'flex';
document.getElementById('email-approval-comment').value = '';
}
function hideEmailApprovalDialog() {
document.getElementById('email-approval-dialog').style.display = 'none';
currentEmailVersionId = null;
}
async function submitEmailApproval() {
if (!currentEmailVersionId) return;
const comment = document.getElementById('email-approval-comment').value.trim();
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment: comment })
});
if (!res.ok) throw new Error('Fehler');
showToast('Version genehmigt', 'success');
hideEmailApprovalDialog();
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler bei der Genehmigung', 'error');
}
}
async function rejectEmailVersion(versionId) {
const reason = prompt('Ablehnungsgrund:');
if (!reason) return;
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason })
});
if (!res.ok) throw new Error('Fehler');
showToast('Version abgelehnt', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler bei der Ablehnung', 'error');
}
}
async function publishEmailVersion(versionId) {
if (!confirm('Möchten Sie diese Version veröffentlichen? Die vorherige Version wird archiviert.')) return;
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/publish`, {
method: 'POST'
});
if (!res.ok) throw new Error('Fehler');
showToast('Version veröffentlicht!', 'success');
loadEmailTemplateVersions();
} catch (e) {
showToast('Fehler beim Veröffentlichen', 'error');
}
}
async function previewEmailVersionById(versionId) {
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/preview`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (!res.ok) throw new Error('Fehler');
const data = await res.json();
document.getElementById('email-preview-subject').textContent = data.subject;
document.getElementById('email-preview-content').innerHTML = data.body_html;
document.getElementById('email-preview-dialog').style.display = 'flex';
currentEmailVersionId = versionId;
} catch (e) {
showToast('Fehler bei der Vorschau', 'error');
}
}
function previewEmailVersion() {
const subject = document.getElementById('email-version-subject').value;
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
document.getElementById('email-preview-subject').textContent = subject;
document.getElementById('email-preview-content').innerHTML = bodyHtml;
document.getElementById('email-preview-dialog').style.display = 'flex';
}
function hideEmailPreview() {
document.getElementById('email-preview-dialog').style.display = 'none';
}
async function sendTestEmail() {
const email = document.getElementById('email-test-address').value.trim();
if (!email) {
showToast('Bitte geben Sie eine E-Mail-Adresse ein', 'error');
return;
}
if (!currentEmailVersionId) {
showToast('Keine Version ausgewählt', 'error');
return;
}
try {
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/send-test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email })
});
if (!res.ok) throw new Error('Fehler');
showToast('Test-E-Mail gesendet!', 'success');
} catch (e) {
showToast('Fehler beim Senden der Test-E-Mail', 'error');
}
}
async function initializeEmailTemplates() {
if (!confirm('Möchten Sie alle Standard-E-Mail-Templates initialisieren?')) return;
try {
const res = await fetch('/api/consent/admin/email-templates/initialize', {
method: 'POST'
});
if (!res.ok) throw new Error('Fehler');
showToast('Templates initialisiert!', 'success');
loadEmailTemplates();
} catch (e) {
showToast('Fehler bei der Initialisierung', 'error');
}
}
// E-Mail Editor Helpers
function formatEmailDoc(command) {
document.execCommand(command, false, null);
document.getElementById('email-version-editor').focus();
}
function formatEmailBlock(tag) {
document.execCommand('formatBlock', false, '<' + tag + '>');
document.getElementById('email-version-editor').focus();
}
function insertEmailVariable() {
const variable = prompt('Variablenname eingeben (z.B. user_name, reset_link):');
if (variable) {
document.execCommand('insertText', false, '{{' + variable + '}}');
}
}
function insertEmailLink() {
const url = prompt('Link-URL:');
if (url) {
const text = prompt('Link-Text:', url);
document.execCommand('insertHTML', false, `<a href="${url}" style="color: #5B21B6;">${text}</a>`);
}
}
function insertEmailButton() {
const url = prompt('Button-Link:');
if (url) {
const text = prompt('Button-Text:', 'Klicken');
const buttonHtml = `<table cellpadding="0" cellspacing="0" style="margin: 16px 0;"><tr><td style="background: #5B21B6; border-radius: 6px; padding: 12px 24px;"><a href="${url}" style="color: white; text-decoration: none; font-weight: 600;">${text}</a></td></tr></table>`;
document.execCommand('insertHTML', false, buttonHtml);
}
}
</script>
</div>
</body>
</html>
"""