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>
771 lines
20 KiB
Python
771 lines
20 KiB
Python
"""
|
|
Companion Dashboard Module - Begleiter-Modus UI.
|
|
|
|
Das Dashboard zeigt:
|
|
- Aktuelle Phase im Schuljahr
|
|
- Priorisierte Vorschläge
|
|
- Fortschritts-Anzeige
|
|
- Kommende Termine
|
|
"""
|
|
|
|
|
|
def get_companion_css() -> str:
|
|
"""CSS für das Companion Dashboard."""
|
|
return """
|
|
/* Companion Dashboard Styles */
|
|
.companion-container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
|
|
.companion-header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.companion-header h1 {
|
|
font-size: 28px;
|
|
color: #1a1a2e;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.companion-header .phase-badge {
|
|
display: inline-block;
|
|
background: linear-gradient(135deg, #6C1B1B, #8B2525);
|
|
color: white;
|
|
padding: 6px 16px;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Phase Indicator */
|
|
.phase-indicator {
|
|
background: #f8f9fa;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.phase-timeline {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
position: relative;
|
|
padding: 0 10px;
|
|
}
|
|
|
|
.phase-timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 15px;
|
|
left: 30px;
|
|
right: 30px;
|
|
height: 3px;
|
|
background: #e0e0e0;
|
|
z-index: 0;
|
|
}
|
|
|
|
.phase-step {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
z-index: 1;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.phase-dot {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
background: #e0e0e0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 8px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.phase-dot.completed {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.phase-dot.current {
|
|
background: #6C1B1B;
|
|
box-shadow: 0 0 0 4px rgba(108, 27, 27, 0.2);
|
|
}
|
|
|
|
.phase-dot .material-icons {
|
|
font-size: 16px;
|
|
color: white;
|
|
}
|
|
|
|
.phase-label {
|
|
font-size: 11px;
|
|
color: #666;
|
|
text-align: center;
|
|
max-width: 70px;
|
|
}
|
|
|
|
.phase-label.current {
|
|
color: #6C1B1B;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Suggestions List */
|
|
.suggestions-section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #1a1a2e;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.suggestion-card {
|
|
display: flex;
|
|
align-items: center;
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
transition: all 0.2s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.suggestion-card:hover {
|
|
border-color: #6C1B1B;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.priority-bar {
|
|
width: 4px;
|
|
height: 50px;
|
|
border-radius: 2px;
|
|
margin-right: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.priority-bar.urgent { background: #ef4444; }
|
|
.priority-bar.high { background: #f97316; }
|
|
.priority-bar.medium { background: #3b82f6; }
|
|
.priority-bar.low { background: #9ca3af; }
|
|
|
|
.suggestion-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
background: #f3f4f6;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.suggestion-icon .material-icons {
|
|
color: #6C1B1B;
|
|
}
|
|
|
|
.suggestion-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.suggestion-title {
|
|
font-weight: 600;
|
|
color: #1a1a2e;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.suggestion-description {
|
|
font-size: 13px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.suggestion-action {
|
|
color: #6C1B1B;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.suggestion-time {
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
background: #f0fdf4;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.empty-state .material-icons {
|
|
font-size: 48px;
|
|
color: #22c55e;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
color: #166534;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.empty-state p {
|
|
color: #6b7280;
|
|
}
|
|
|
|
/* Stats Cards */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
gap: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
padding: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: #6C1B1B;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Progress Card */
|
|
.progress-card {
|
|
background: #eff6ff;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.progress-title {
|
|
font-weight: 600;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.progress-percentage {
|
|
font-weight: 700;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.progress-bar-container {
|
|
background: #dbeafe;
|
|
border-radius: 10px;
|
|
height: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-bar-fill {
|
|
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
|
|
height: 100%;
|
|
border-radius: 10px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.progress-milestones {
|
|
margin-top: 12px;
|
|
font-size: 13px;
|
|
color: #4b5563;
|
|
}
|
|
|
|
/* Events Card */
|
|
.events-card {
|
|
background: #fefce8;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.event-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid #fef08a;
|
|
}
|
|
|
|
.event-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.event-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
background: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.event-icon .material-icons {
|
|
color: #ca8a04;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.event-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.event-title {
|
|
font-weight: 500;
|
|
color: #1a1a2e;
|
|
}
|
|
|
|
.event-date {
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.event-badge {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #ca8a04;
|
|
background: white;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
/* Mode Toggle */
|
|
.mode-toggle {
|
|
display: flex;
|
|
background: #f3f4f6;
|
|
border-radius: 8px;
|
|
padding: 4px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.mode-btn {
|
|
flex: 1;
|
|
padding: 10px 16px;
|
|
border: none;
|
|
background: transparent;
|
|
border-radius: 6px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.mode-btn.active {
|
|
background: #6C1B1B;
|
|
color: white;
|
|
}
|
|
|
|
.mode-btn:not(.active):hover {
|
|
background: #e5e7eb;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 600px) {
|
|
.phase-timeline {
|
|
overflow-x: auto;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.suggestion-card {
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
"""
|
|
|
|
|
|
def get_companion_html() -> str:
|
|
"""HTML Template für das Companion Dashboard."""
|
|
return """
|
|
<div class="companion-container" id="companionDashboard">
|
|
<!-- Mode Toggle -->
|
|
<div class="mode-toggle">
|
|
<button class="mode-btn active" id="modeCompanion" onclick="setMode('companion')">
|
|
<span class="material-icons" style="font-size: 16px; vertical-align: middle; margin-right: 4px;">assistant</span>
|
|
Begleiter
|
|
</button>
|
|
<button class="mode-btn" id="modeClassic" onclick="setMode('classic')">
|
|
<span class="material-icons" style="font-size: 16px; vertical-align: middle; margin-right: 4px;">dashboard</span>
|
|
Klassisch
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="companion-header">
|
|
<h1>Was ist jetzt wichtig?</h1>
|
|
<span class="phase-badge" id="phaseBadge">Lädt...</span>
|
|
</div>
|
|
|
|
<!-- Phase Indicator -->
|
|
<div class="phase-indicator">
|
|
<div class="phase-timeline" id="phaseTimeline">
|
|
<!-- Dynamisch gefüllt -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="stats-grid" id="statsGrid">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statClasses">0</div>
|
|
<div class="stat-label">Klassen</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statStudents">0</div>
|
|
<div class="stat-label">Schüler</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statUnits">0</div>
|
|
<div class="stat-label">Lerneinheiten</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statGrades">0</div>
|
|
<div class="stat-label">Noten</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Card -->
|
|
<div class="progress-card" id="progressCard">
|
|
<div class="progress-header">
|
|
<span class="progress-title">Fortschritt in dieser Phase</span>
|
|
<span class="progress-percentage" id="progressPercent">0%</span>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-fill" id="progressBar" style="width: 0%"></div>
|
|
</div>
|
|
<div class="progress-milestones" id="progressMilestones">
|
|
0 von 0 Meilensteinen erreicht
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Suggestions -->
|
|
<div class="suggestions-section">
|
|
<div class="section-title">
|
|
<span class="material-icons">lightbulb</span>
|
|
Empfohlene Aktionen
|
|
</div>
|
|
<div id="suggestionsList">
|
|
<!-- Dynamisch gefüllt -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Events -->
|
|
<div class="events-card" id="eventsCard" style="display: none;">
|
|
<div class="section-title" style="margin-bottom: 12px;">
|
|
<span class="material-icons">event</span>
|
|
Kommende Termine
|
|
</div>
|
|
<div id="eventsList">
|
|
<!-- Dynamisch gefüllt -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def get_companion_js() -> str:
|
|
"""JavaScript für das Companion Dashboard."""
|
|
return """
|
|
// Companion Dashboard JavaScript
|
|
|
|
let companionData = null;
|
|
let currentMode = 'companion';
|
|
|
|
async function loadCompanionDashboard() {
|
|
try {
|
|
const response = await fetch('/api/state/dashboard?teacher_id=demo-teacher');
|
|
companionData = await response.json();
|
|
renderDashboard();
|
|
} catch (error) {
|
|
console.error('Error loading dashboard:', error);
|
|
showError('Dashboard konnte nicht geladen werden');
|
|
}
|
|
}
|
|
|
|
function renderDashboard() {
|
|
if (!companionData) return;
|
|
|
|
// Phase Badge
|
|
document.getElementById('phaseBadge').textContent = companionData.context.phase_display_name;
|
|
|
|
// Phase Timeline
|
|
renderPhaseTimeline();
|
|
|
|
// Stats
|
|
document.getElementById('statClasses').textContent = companionData.stats.classes_count || 0;
|
|
document.getElementById('statStudents').textContent = companionData.stats.students_count || 0;
|
|
document.getElementById('statUnits').textContent = companionData.stats.learning_units_created || 0;
|
|
document.getElementById('statGrades').textContent = companionData.stats.grades_entered || 0;
|
|
|
|
// Progress
|
|
const progress = companionData.progress;
|
|
document.getElementById('progressPercent').textContent = Math.round(progress.percentage) + '%';
|
|
document.getElementById('progressBar').style.width = progress.percentage + '%';
|
|
document.getElementById('progressMilestones').textContent =
|
|
`${progress.completed} von ${progress.total} Meilensteinen erreicht`;
|
|
|
|
// Suggestions
|
|
renderSuggestions();
|
|
|
|
// Events
|
|
renderEvents();
|
|
}
|
|
|
|
function renderPhaseTimeline() {
|
|
const container = document.getElementById('phaseTimeline');
|
|
container.innerHTML = '';
|
|
|
|
companionData.phases.forEach(phase => {
|
|
const step = document.createElement('div');
|
|
step.className = 'phase-step';
|
|
step.onclick = () => console.log('Phase clicked:', phase.phase);
|
|
|
|
const dot = document.createElement('div');
|
|
dot.className = 'phase-dot';
|
|
if (phase.is_completed) {
|
|
dot.classList.add('completed');
|
|
dot.innerHTML = '<span class="material-icons">check</span>';
|
|
} else if (phase.is_current) {
|
|
dot.classList.add('current');
|
|
dot.innerHTML = '<span class="material-icons">circle</span>';
|
|
}
|
|
|
|
const label = document.createElement('div');
|
|
label.className = 'phase-label';
|
|
if (phase.is_current) label.classList.add('current');
|
|
label.textContent = phase.short_name;
|
|
|
|
step.appendChild(dot);
|
|
step.appendChild(label);
|
|
container.appendChild(step);
|
|
});
|
|
}
|
|
|
|
function renderSuggestions() {
|
|
const container = document.getElementById('suggestionsList');
|
|
|
|
if (!companionData.suggestions || companionData.suggestions.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<span class="material-icons">check_circle</span>
|
|
<h3>Alles erledigt!</h3>
|
|
<p>Keine offenen Aufgaben. Gute Arbeit!</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = companionData.suggestions.map(s => `
|
|
<div class="suggestion-card" onclick="navigateTo('${s.action_target}')">
|
|
<div class="priority-bar ${s.priority.toLowerCase()}"></div>
|
|
<div class="suggestion-icon">
|
|
<span class="material-icons">${s.icon}</span>
|
|
</div>
|
|
<div class="suggestion-content">
|
|
<div class="suggestion-title">${s.title}</div>
|
|
<div class="suggestion-description">${s.description}</div>
|
|
<div class="suggestion-time">
|
|
<span class="material-icons" style="font-size: 14px; vertical-align: middle;">schedule</span>
|
|
ca. ${s.estimated_time} Min.
|
|
</div>
|
|
</div>
|
|
<div class="suggestion-action">
|
|
Los <span class="material-icons" style="font-size: 18px;">arrow_forward</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderEvents() {
|
|
const container = document.getElementById('eventsList');
|
|
const card = document.getElementById('eventsCard');
|
|
|
|
if (!companionData.upcoming_events || companionData.upcoming_events.length === 0) {
|
|
card.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
card.style.display = 'block';
|
|
|
|
const getEventIcon = (type) => {
|
|
const icons = {
|
|
'exam': 'quiz',
|
|
'parent_meeting': 'groups',
|
|
'deadline': 'alarm',
|
|
'default': 'event'
|
|
};
|
|
return icons[type] || icons.default;
|
|
};
|
|
|
|
container.innerHTML = companionData.upcoming_events.map(e => `
|
|
<div class="event-item">
|
|
<div class="event-icon">
|
|
<span class="material-icons">${getEventIcon(e.type)}</span>
|
|
</div>
|
|
<div class="event-info">
|
|
<div class="event-title">${e.title}</div>
|
|
<div class="event-date">${formatDate(e.date)}</div>
|
|
</div>
|
|
<div class="event-badge">
|
|
${e.in_days === 0 ? 'Heute' : `In ${e.in_days} Tagen`}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function formatDate(isoString) {
|
|
const date = new Date(isoString);
|
|
return date.toLocaleDateString('de-DE', {
|
|
weekday: 'short',
|
|
day: 'numeric',
|
|
month: 'short'
|
|
});
|
|
}
|
|
|
|
function navigateTo(target) {
|
|
console.log('Navigate to:', target);
|
|
// In echter App: window.location.href = target;
|
|
// Oder: router.push(target);
|
|
|
|
// Für Demo: Zeige Nachricht
|
|
showToast(`Navigiere zu: ${target}`);
|
|
}
|
|
|
|
function setMode(mode) {
|
|
currentMode = mode;
|
|
|
|
document.getElementById('modeCompanion').classList.toggle('active', mode === 'companion');
|
|
document.getElementById('modeClassic').classList.toggle('active', mode === 'classic');
|
|
|
|
if (mode === 'classic') {
|
|
showToast('Klassischer Modus - Navigation zu Dashboard');
|
|
// window.location.href = '/studio';
|
|
}
|
|
}
|
|
|
|
function showToast(message) {
|
|
// Einfache Toast-Nachricht
|
|
const toast = document.createElement('div');
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #1a1a2e;
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
z-index: 1000;
|
|
animation: fadeIn 0.3s ease;
|
|
`;
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.opacity = '0';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 2000);
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('suggestionsList').innerHTML = `
|
|
<div class="empty-state" style="background: #fef2f2;">
|
|
<span class="material-icons" style="color: #ef4444;">error</span>
|
|
<h3 style="color: #b91c1c;">Fehler</h3>
|
|
<p>${message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Milestone abschließen
|
|
async function completeMilestone(milestone) {
|
|
try {
|
|
const response = await fetch('/api/state/milestone?teacher_id=demo-teacher', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ milestone })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(`Meilenstein "${milestone}" abgeschlossen!`);
|
|
if (result.new_phase) {
|
|
showToast(`Neue Phase: ${result.new_phase}`);
|
|
}
|
|
loadCompanionDashboard(); // Reload
|
|
}
|
|
} catch (error) {
|
|
console.error('Error completing milestone:', error);
|
|
}
|
|
}
|
|
|
|
// Initial laden
|
|
document.addEventListener('DOMContentLoaded', loadCompanionDashboard);
|
|
"""
|
|
|
|
|
|
class CompanionModule:
|
|
"""
|
|
Companion Dashboard Module für den Begleiter-Modus.
|
|
|
|
Zeigt:
|
|
- Aktuelle Phase im Schuljahr
|
|
- Priorisierte Vorschläge
|
|
- Fortschritts-Anzeige
|
|
- Kommende Termine
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.name = "companion"
|
|
self.display_name = "Begleiter"
|
|
self.icon = "assistant"
|
|
|
|
def get_css(self) -> str:
|
|
return get_companion_css()
|
|
|
|
def get_html(self) -> str:
|
|
return get_companion_html()
|
|
|
|
def get_js(self) -> str:
|
|
return get_companion_js()
|
|
|
|
def render(self) -> dict:
|
|
"""Rendert das komplette Modul."""
|
|
return {
|
|
"css": self.get_css(),
|
|
"html": self.get_html(),
|
|
"js": self.get_js(),
|
|
}
|