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/modules/lehrer_dashboard.py
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

890 lines
23 KiB
Python

"""
Lehrer-Dashboard Modul fuer das BreakPilot Studio.
Ein frei konfigurierbares Dashboard mit Drag & Drop Widget-System.
Lehrer koennen ihre persoenliche Startseite aus verschiedenen Widgets zusammenstellen.
"""
from .widgets import (
TodosWidget,
SchnellzugriffWidget,
NotizenWidget,
StundenplanWidget,
KlassenWidget,
FehlzeitenWidget,
ArbeitenWidget,
NachrichtenWidget,
MatrixWidget,
AlertsWidget,
StatistikWidget,
KalenderWidget,
)
class LehrerDashboardModule:
"""
Haupt-Modul fuer das konfigurierbare Lehrer-Dashboard.
"""
@staticmethod
def get_css() -> str:
# Sammle CSS von allen Widgets
widget_css = "\n".join([
TodosWidget.get_css(),
SchnellzugriffWidget.get_css(),
NotizenWidget.get_css(),
StundenplanWidget.get_css(),
KlassenWidget.get_css(),
FehlzeitenWidget.get_css(),
ArbeitenWidget.get_css(),
NachrichtenWidget.get_css(),
MatrixWidget.get_css(),
AlertsWidget.get_css(),
StatistikWidget.get_css(),
KalenderWidget.get_css(),
])
return f"""
/* ===== Lehrer-Dashboard Styles ===== */
.lehrer-dashboard-container {{
padding: 24px;
max-width: 1400px;
margin: 0 auto;
}}
/* Dashboard Header */
.dashboard-header {{
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}}
.dashboard-greeting {{
font-size: 24px;
font-weight: 700;
color: var(--bp-text, #e5e7eb);
margin-bottom: 4px;
}}
.dashboard-date {{
font-size: 14px;
color: var(--bp-text-muted, #9ca3af);
}}
.dashboard-actions {{
display: flex;
gap: 8px;
}}
.dashboard-edit-btn {{
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 8px;
color: var(--bp-text, #e5e7eb);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}}
.dashboard-edit-btn:hover {{
background: var(--bp-surface-elevated, #334155);
border-color: var(--bp-primary, #6C1B1B);
}}
.dashboard-edit-btn.active {{
background: var(--bp-primary, #6C1B1B);
border-color: var(--bp-primary, #6C1B1B);
color: white;
}}
/* Widget Grid */
.dashboard-grid {{
display: flex;
flex-direction: column;
gap: 16px;
}}
.dashboard-row {{
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}}
.dashboard-row.single {{
grid-template-columns: 1fr;
}}
/* Widget Container */
.dashboard-widget {{
position: relative;
min-height: 200px;
}}
.dashboard-widget.full {{
grid-column: 1 / -1;
}}
/* Widget Settings Button */
.widget-settings-btn {{
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--bp-text-muted, #9ca3af);
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
font-size: 14px;
}}
.widget-settings-btn:hover {{
background: var(--bp-surface-elevated, #334155);
color: var(--bp-text, #e5e7eb);
}}
/* ===== Edit Mode Styles ===== */
.dashboard-edit-mode .widget-catalog {{
display: block !important;
}}
.dashboard-edit-mode .dashboard-widget {{
position: relative;
}}
.dashboard-edit-mode .dashboard-widget::after {{
content: '';
position: absolute;
inset: 0;
border: 2px dashed var(--bp-border, #475569);
border-radius: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
}}
.dashboard-edit-mode .dashboard-widget:hover::after {{
opacity: 1;
}}
.dashboard-edit-mode .widget-remove-btn {{
display: flex !important;
}}
/* Widget Remove Button */
.widget-remove-btn {{
display: none;
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
background: rgba(239, 68, 68, 0.9);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 14px;
z-index: 10;
transition: all 0.2s;
}}
.widget-remove-btn:hover {{
background: #ef4444;
transform: scale(1.1);
}}
/* Widget Catalog */
.widget-catalog {{
display: none;
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
}}
.widget-catalog-title {{
font-size: 14px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
margin-bottom: 12px;
}}
.widget-catalog-grid {{
display: flex;
flex-wrap: wrap;
gap: 8px;
}}
.widget-catalog-item {{
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: var(--bp-bg, #0f172a);
border: 1px solid var(--bp-border-subtle, rgba(255,255,255,0.1));
border-radius: 8px;
cursor: grab;
transition: all 0.2s;
user-select: none;
}}
.widget-catalog-item:hover {{
border-color: var(--bp-primary, #6C1B1B);
transform: translateY(-2px);
}}
.widget-catalog-item:active {{
cursor: grabbing;
}}
.widget-catalog-item.dragging {{
opacity: 0.5;
}}
.widget-catalog-item.disabled {{
opacity: 0.4;
cursor: not-allowed;
}}
.widget-catalog-item-icon {{
font-size: 16px;
}}
.widget-catalog-item-name {{
font-size: 12px;
font-weight: 500;
color: var(--bp-text, #e5e7eb);
}}
/* Drop Zone */
.drop-zone {{
display: none;
min-height: 100px;
border: 2px dashed var(--bp-border, #475569);
border-radius: 12px;
background: var(--bp-bg, #0f172a);
transition: all 0.2s;
}}
.dashboard-edit-mode .drop-zone {{
display: flex;
align-items: center;
justify-content: center;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
}}
.drop-zone.drag-over {{
border-color: var(--bp-accent, #5ABF60);
background: rgba(90, 191, 96, 0.1);
color: var(--bp-accent, #5ABF60);
}}
/* Add Row Button */
.add-row-btn {{
display: none;
width: 100%;
padding: 16px;
background: transparent;
border: 2px dashed var(--bp-border, #475569);
border-radius: 12px;
color: var(--bp-text-muted, #9ca3af);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}}
.dashboard-edit-mode .add-row-btn {{
display: block;
}}
.add-row-btn:hover {{
border-color: var(--bp-accent, #5ABF60);
color: var(--bp-accent, #5ABF60);
}}
/* Widget Settings Modal */
.widget-settings-modal {{
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
align-items: center;
justify-content: center;
}}
.widget-settings-modal.active {{
display: flex;
}}
.widget-settings-content {{
background: var(--bp-surface, #1e293b);
border: 1px solid var(--bp-border, #475569);
border-radius: 16px;
padding: 24px;
max-width: 400px;
width: 90%;
}}
.widget-settings-title {{
font-size: 18px;
font-weight: 600;
color: var(--bp-text, #e5e7eb);
margin-bottom: 16px;
}}
.widget-settings-close {{
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--bp-text-muted, #9ca3af);
cursor: pointer;
border-radius: 8px;
font-size: 18px;
}}
.widget-settings-close:hover {{
background: var(--bp-surface-elevated, #334155);
}}
/* Responsive */
@media (max-width: 768px) {{
.lehrer-dashboard-container {{
padding: 16px;
}}
.dashboard-header {{
flex-direction: column;
gap: 16px;
}}
.dashboard-row {{
grid-template-columns: 1fr;
}}
.dashboard-greeting {{
font-size: 20px;
}}
.widget-catalog-grid {{
flex-direction: column;
}}
.widget-catalog-item {{
width: 100%;
}}
}}
/* Widget CSS */
{widget_css}
"""
@staticmethod
def get_html() -> str:
return """
<div class="panel panel-lehrer-dashboard" id="panel-lehrer-dashboard" style="display: none;">
<div class="lehrer-dashboard-container">
<!-- Header -->
<div class="dashboard-header">
<div>
<div class="dashboard-greeting" id="dashboard-greeting">Guten Tag!</div>
<div class="dashboard-date" id="dashboard-date"></div>
</div>
<div class="dashboard-actions">
<button class="dashboard-edit-btn" id="dashboard-edit-btn" onclick="toggleDashboardEditMode()">
<span>&#127912;</span>
<span id="edit-btn-text">Anpassen</span>
</button>
</div>
</div>
<!-- Widget Catalog (Edit Mode) -->
<div class="widget-catalog" id="widget-catalog">
<div class="widget-catalog-title">Widget-Katalog (ziehen Sie Widgets auf Ihr Dashboard):</div>
<div class="widget-catalog-grid" id="widget-catalog-grid">
<!-- Wird dynamisch gefuellt -->
</div>
</div>
<!-- Dashboard Grid -->
<div class="dashboard-grid" id="dashboard-grid">
<!-- Wird dynamisch gefuellt -->
</div>
<!-- Add Row Button -->
<button class="add-row-btn" onclick="addDashboardRow()">+ Neue Reihe hinzufuegen</button>
</div>
<!-- Widget Settings Modal -->
<div class="widget-settings-modal" id="widget-settings-modal">
<div class="widget-settings-content">
<button class="widget-settings-close" onclick="closeWidgetSettings()">&times;</button>
<div class="widget-settings-title" id="widget-settings-title">Widget-Einstellungen</div>
<div id="widget-settings-body">
<!-- Wird dynamisch gefuellt -->
</div>
</div>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
# Sammle JS von allen Widgets
widget_js = "\n".join([
TodosWidget.get_js(),
SchnellzugriffWidget.get_js(),
NotizenWidget.get_js(),
StundenplanWidget.get_js(),
KlassenWidget.get_js(),
FehlzeitenWidget.get_js(),
ArbeitenWidget.get_js(),
NachrichtenWidget.get_js(),
MatrixWidget.get_js(),
AlertsWidget.get_js(),
StatistikWidget.get_js(),
KalenderWidget.get_js(),
])
return f"""
// ===== Lehrer-Dashboard JavaScript =====
const DASHBOARD_LAYOUT_KEY = 'bp-dashboard-layout';
const LEHRER_PROFIL_KEY = 'bp-lehrer-profil';
let dashboardEditMode = false;
let lehrerDashboardInitialized = false;
// Widget Registry
const WidgetRegistry = {{
stundenplan: {{
id: 'stundenplan',
name: 'Stundenplan',
icon: '&#128197;',
color: '#3b82f6',
defaultWidth: 'half',
init: initStundenplanWidget,
getHtml: () => `{StundenplanWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
klassen: {{
id: 'klassen',
name: 'Meine Klassen',
icon: '&#128202;',
color: '#8b5cf6',
defaultWidth: 'half',
init: initKlassenWidget,
getHtml: () => `{KlassenWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
fehlzeiten: {{
id: 'fehlzeiten',
name: 'Fehlzeiten',
icon: '&#9888;',
color: '#ef4444',
defaultWidth: 'half',
init: initFehlzeitenWidget,
getHtml: () => `{FehlzeitenWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
arbeiten: {{
id: 'arbeiten',
name: 'Arbeiten',
icon: '&#128221;',
color: '#f59e0b',
defaultWidth: 'half',
init: initArbeitenWidget,
getHtml: () => `{ArbeitenWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
todos: {{
id: 'todos',
name: 'To-Dos',
icon: '&#10003;',
color: '#10b981',
defaultWidth: 'half',
init: initTodosWidget,
getHtml: () => `{TodosWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
nachrichten: {{
id: 'nachrichten',
name: 'E-Mails',
icon: '&#128231;',
color: '#06b6d4',
defaultWidth: 'half',
init: initNachrichtenWidget,
getHtml: () => `{NachrichtenWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
matrix: {{
id: 'matrix',
name: 'Matrix-Chat',
icon: '&#128172;',
color: '#8b5cf6',
defaultWidth: 'half',
init: initMatrixWidget,
getHtml: () => `{MatrixWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
alerts: {{
id: 'alerts',
name: 'Google Alerts',
icon: '&#128276;',
color: '#f59e0b',
defaultWidth: 'half',
init: initAlertsWidget,
getHtml: () => `{AlertsWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
statistik: {{
id: 'statistik',
name: 'Statistik',
icon: '&#128200;',
color: '#3b82f6',
defaultWidth: 'full',
init: initStatistikWidget,
getHtml: () => `{StatistikWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
schnellzugriff: {{
id: 'schnellzugriff',
name: 'Schnellzugriff',
icon: '&#9889;',
color: '#6b7280',
defaultWidth: 'full',
init: initSchnellzugriffWidget,
getHtml: () => `{SchnellzugriffWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
notizen: {{
id: 'notizen',
name: 'Notizen',
icon: '&#128203;',
color: '#fbbf24',
defaultWidth: 'half',
init: initNotizenWidget,
getHtml: () => `{NotizenWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}},
kalender: {{
id: 'kalender',
name: 'Termine',
icon: '&#128198;',
color: '#ec4899',
defaultWidth: 'half',
init: initKalenderWidget,
getHtml: () => `{KalenderWidget.get_html().replace('`', '\\`').replace('${', '\\${')}`
}}
}};
// Default Layout
function getDefaultLayout() {{
return {{
version: 1,
rows: [
{{
id: 'row-1',
widgets: [
{{ widgetId: 'stundenplan', width: 'half' }},
{{ widgetId: 'klassen', width: 'half' }}
]
}},
{{
id: 'row-2',
widgets: [
{{ widgetId: 'fehlzeiten', width: 'half' }},
{{ widgetId: 'arbeiten', width: 'half' }}
]
}},
{{
id: 'row-3',
widgets: [
{{ widgetId: 'todos', width: 'half' }},
{{ widgetId: 'nachrichten', width: 'half' }}
]
}},
{{
id: 'row-4',
widgets: [
{{ widgetId: 'schnellzugriff', width: 'full' }}
]
}}
]
}};
}}
function loadDashboardLayout() {{
const stored = localStorage.getItem(DASHBOARD_LAYOUT_KEY);
return stored ? JSON.parse(stored) : getDefaultLayout();
}}
function saveDashboardLayout(layout) {{
localStorage.setItem(DASHBOARD_LAYOUT_KEY, JSON.stringify(layout));
}}
function getGreeting() {{
const hour = new Date().getHours();
if (hour < 12) return 'Guten Morgen';
if (hour < 18) return 'Guten Tag';
return 'Guten Abend';
}}
function getLehrerName() {{
const profil = localStorage.getItem(LEHRER_PROFIL_KEY);
if (profil) {{
try {{
return JSON.parse(profil).name || 'Lehrer';
}} catch (e) {{}}
}}
return '';
}}
function formatDashboardDate() {{
return new Date().toLocaleDateString('de-DE', {{
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}});
}}
function renderDashboardHeader() {{
const greetingEl = document.getElementById('dashboard-greeting');
const dateEl = document.getElementById('dashboard-date');
if (greetingEl) {{
const name = getLehrerName();
greetingEl.textContent = `${{getGreeting()}}${{name ? ', ' + name : ''}}!`;
}}
if (dateEl) {{
dateEl.textContent = formatDashboardDate();
}}
}}
function renderWidgetCatalog() {{
const grid = document.getElementById('widget-catalog-grid');
if (!grid) return;
const layout = loadDashboardLayout();
const usedWidgets = new Set();
layout.rows.forEach(row => {{
row.widgets.forEach(w => usedWidgets.add(w.widgetId));
}});
grid.innerHTML = Object.values(WidgetRegistry).map(widget => {{
const isUsed = usedWidgets.has(widget.id);
return `
<div class="widget-catalog-item ${{isUsed ? 'disabled' : ''}}"
draggable="${{!isUsed}}"
data-widget-id="${{widget.id}}"
ondragstart="handleWidgetDragStart(event)"
ondragend="handleWidgetDragEnd(event)">
<span class="widget-catalog-item-icon">${{widget.icon}}</span>
<span class="widget-catalog-item-name">${{widget.name}}</span>
</div>
`;
}}).join('');
}}
function renderDashboardGrid() {{
const grid = document.getElementById('dashboard-grid');
if (!grid) return;
const layout = loadDashboardLayout();
grid.innerHTML = layout.rows.map((row, rowIndex) => {{
const isSingleFull = row.widgets.length === 1 && row.widgets[0].width === 'full';
return `
<div class="dashboard-row ${{isSingleFull ? 'single' : ''}}" data-row-id="${{row.id}}">
${{row.widgets.map((w, widgetIndex) => {{
const widget = WidgetRegistry[w.widgetId];
if (!widget) return '';
return `
<div class="dashboard-widget ${{w.width}}" data-widget-id="${{w.widgetId}}">
<button class="widget-remove-btn" onclick="removeWidget('${{row.id}}', ${{widgetIndex}})">&times;</button>
${{widget.getHtml()}}
</div>
`;
}}).join('')}}
${{dashboardEditMode && row.widgets.length < 2 ? `
<div class="drop-zone"
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)"
ondrop="handleDrop(event, '${{row.id}}')"
data-row-id="${{row.id}}">
Widget hier ablegen
</div>
` : ''}}
</div>
`;
}}).join('');
// Initialize all widgets
layout.rows.forEach(row => {{
row.widgets.forEach(w => {{
const widget = WidgetRegistry[w.widgetId];
if (widget && widget.init) {{
setTimeout(() => widget.init(), 0);
}}
}});
}});
}}
function toggleDashboardEditMode() {{
dashboardEditMode = !dashboardEditMode;
const container = document.querySelector('.lehrer-dashboard-container');
const btn = document.getElementById('dashboard-edit-btn');
const btnText = document.getElementById('edit-btn-text');
if (container) {{
if (dashboardEditMode) {{
container.classList.add('dashboard-edit-mode');
}} else {{
container.classList.remove('dashboard-edit-mode');
}}
}}
if (btn) {{
btn.classList.toggle('active', dashboardEditMode);
}}
if (btnText) {{
btnText.textContent = dashboardEditMode ? 'Fertig' : 'Anpassen';
}}
renderWidgetCatalog();
renderDashboardGrid();
}}
function handleWidgetDragStart(event) {{
const widgetId = event.target.dataset.widgetId;
event.dataTransfer.setData('widget-id', widgetId);
event.target.classList.add('dragging');
}}
function handleWidgetDragEnd(event) {{
event.target.classList.remove('dragging');
}}
function handleDragOver(event) {{
event.preventDefault();
event.currentTarget.classList.add('drag-over');
}}
function handleDragLeave(event) {{
event.currentTarget.classList.remove('drag-over');
}}
function handleDrop(event, rowId) {{
event.preventDefault();
event.currentTarget.classList.remove('drag-over');
const widgetId = event.dataTransfer.getData('widget-id');
if (!widgetId || !WidgetRegistry[widgetId]) return;
const layout = loadDashboardLayout();
const row = layout.rows.find(r => r.id === rowId);
if (row && row.widgets.length < 2) {{
const widget = WidgetRegistry[widgetId];
row.widgets.push({{
widgetId: widgetId,
width: widget.defaultWidth === 'full' ? 'full' : 'half'
}});
saveDashboardLayout(layout);
renderWidgetCatalog();
renderDashboardGrid();
}}
}}
function removeWidget(rowId, widgetIndex) {{
const layout = loadDashboardLayout();
const rowIndex = layout.rows.findIndex(r => r.id === rowId);
if (rowIndex !== -1) {{
layout.rows[rowIndex].widgets.splice(widgetIndex, 1);
// Remove empty rows
if (layout.rows[rowIndex].widgets.length === 0) {{
layout.rows.splice(rowIndex, 1);
}}
saveDashboardLayout(layout);
renderWidgetCatalog();
renderDashboardGrid();
}}
}}
function addDashboardRow() {{
const layout = loadDashboardLayout();
const newRowId = 'row-' + Date.now();
layout.rows.push({{
id: newRowId,
widgets: []
}});
saveDashboardLayout(layout);
renderDashboardGrid();
}}
function openWidgetSettings(widgetId) {{
const modal = document.getElementById('widget-settings-modal');
const title = document.getElementById('widget-settings-title');
const body = document.getElementById('widget-settings-body');
if (!modal || !title || !body) return;
const widget = WidgetRegistry[widgetId];
if (!widget) return;
title.textContent = widget.name + ' - Einstellungen';
body.innerHTML = `
<p style="color: var(--bp-text-muted); font-size: 13px; text-align: center; padding: 24px;">
Widget-Einstellungen werden in einer zukuenftigen Version verfuegbar sein.
</p>
`;
modal.classList.add('active');
}}
function closeWidgetSettings() {{
const modal = document.getElementById('widget-settings-modal');
if (modal) {{
modal.classList.remove('active');
}}
}}
function loadLehrerDashboardModule() {{
if (lehrerDashboardInitialized) {{
console.log('Lehrer-Dashboard already initialized');
return;
}}
console.log('Loading Lehrer-Dashboard Module...');
renderDashboardHeader();
renderWidgetCatalog();
renderDashboardGrid();
lehrerDashboardInitialized = true;
console.log('Lehrer-Dashboard Module loaded successfully');
}}
// Widget JavaScript
{widget_js}
"""