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/alerts_guided.py
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

1583 lines
40 KiB
Python

"""
BreakPilot Studio - Alerts Guided Mode Modul
Dieses Modul implementiert den Guided Mode fuer das Alerts-System:
- 3-Schritt Wizard (Rolle -> Templates -> Bestaetigung)
- InfoCards mit Wichtigkeitsstufen
- 1-Klick Feedback
- Wochenzusammenfassung (Digest)
Zielgruppe: Lehrkraefte, Schulleitungen (nicht IT-affin)
Design-Prinzip: Einfache Bedienung, max. 10 Karten/Tag, deutsche Erklaerungen
"""
class AlertsGuidedModule:
"""Guided Mode fuer das Alerts-System."""
@staticmethod
def get_css() -> str:
"""CSS fuer den Guided Mode."""
return """
/* ==========================================
ALERTS GUIDED MODE STYLES
========================================== */
/* Mode Switcher */
.alerts-mode-switcher {
display: flex;
gap: 8px;
padding: 4px;
background: var(--bp-surface-elevated);
border-radius: 8px;
border: 1px solid var(--bp-border);
}
.mode-btn {
padding: 8px 16px;
border: none;
background: transparent;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: var(--bp-text-muted);
cursor: pointer;
transition: all 0.2s;
}
.mode-btn:hover {
background: var(--bp-surface);
}
.mode-btn.active {
background: var(--bp-primary);
color: white;
}
/* Wizard Container */
.guided-wizard {
display: none;
flex-direction: column;
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
}
.guided-wizard.active {
display: flex;
}
/* Wizard Progress */
.wizard-progress {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 40px;
}
.wizard-step-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.wizard-step-dot {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
background: var(--bp-surface);
border: 2px solid var(--bp-border);
color: var(--bp-text-muted);
}
.wizard-step-dot.active {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
.wizard-step-dot.completed {
background: var(--bp-success);
border-color: var(--bp-success);
color: white;
}
.wizard-step-line {
width: 60px;
height: 2px;
background: var(--bp-border);
}
.wizard-step-line.completed {
background: var(--bp-success);
}
/* Wizard Step Content */
.wizard-step {
display: none;
flex-direction: column;
align-items: center;
}
.wizard-step.active {
display: flex;
}
.wizard-step-title {
font-size: 28px;
font-weight: 700;
color: var(--bp-text);
margin-bottom: 12px;
text-align: center;
}
.wizard-step-description {
font-size: 16px;
color: var(--bp-text-muted);
text-align: center;
max-width: 500px;
margin-bottom: 32px;
}
/* Role Selection */
.role-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
width: 100%;
max-width: 720px;
}
.role-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
background: var(--bp-surface);
border: 2px solid var(--bp-border);
border-radius: 16px;
cursor: pointer;
transition: all 0.2s;
}
.role-card:hover {
border-color: var(--bp-primary);
transform: translateY(-2px);
}
.role-card.selected {
border-color: var(--bp-primary);
background: rgba(var(--bp-primary-rgb), 0.05);
}
.role-card-icon {
font-size: 40px;
margin-bottom: 12px;
}
.role-card-title {
font-size: 18px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 4px;
}
.role-card-description {
font-size: 13px;
color: var(--bp-text-muted);
text-align: center;
}
/* Template Selection */
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
width: 100%;
max-width: 900px;
}
.template-card {
display: flex;
flex-direction: column;
padding: 20px;
background: var(--bp-surface);
border: 2px solid var(--bp-border);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.template-card:hover {
border-color: var(--bp-primary);
}
.template-card.selected {
border-color: var(--bp-primary);
background: rgba(var(--bp-primary-rgb), 0.05);
}
.template-card.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.template-card-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.template-card-icon {
font-size: 28px;
flex-shrink: 0;
}
.template-card-info {
flex: 1;
}
.template-card-name {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 4px;
}
.template-card-desc {
font-size: 13px;
color: var(--bp-text-muted);
line-height: 1.4;
}
.template-card-check {
position: absolute;
top: 12px;
right: 12px;
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid var(--bp-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: white;
background: transparent;
}
.template-card.selected .template-card-check {
background: var(--bp-primary);
border-color: var(--bp-primary);
}
.template-selection-info {
text-align: center;
margin-top: 16px;
font-size: 14px;
color: var(--bp-text-muted);
}
.template-selection-count {
font-weight: 600;
color: var(--bp-primary);
}
/* Confirmation Step */
.confirmation-summary {
width: 100%;
max-width: 500px;
background: var(--bp-surface);
border-radius: 16px;
padding: 24px;
border: 1px solid var(--bp-border);
}
.confirmation-item {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--bp-border);
}
.confirmation-item:last-child {
border-bottom: none;
}
.confirmation-label {
font-size: 14px;
color: var(--bp-text-muted);
}
.confirmation-value {
font-size: 14px;
font-weight: 500;
color: var(--bp-text);
}
.confirmation-templates {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.confirmation-template-tag {
background: var(--bp-primary-soft);
color: var(--bp-primary);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
/* Email Input */
.email-input-group {
margin-top: 24px;
width: 100%;
}
.email-input-label {
font-size: 14px;
font-weight: 500;
color: var(--bp-text);
margin-bottom: 8px;
display: block;
}
.email-input-hint {
font-size: 12px;
color: var(--bp-text-muted);
margin-bottom: 8px;
}
.email-input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--bp-border);
border-radius: 8px;
font-size: 14px;
background: var(--bp-surface);
color: var(--bp-text);
}
.email-input:focus {
outline: none;
border-color: var(--bp-primary);
}
/* Wizard Navigation */
.wizard-nav {
display: flex;
justify-content: space-between;
margin-top: 40px;
width: 100%;
max-width: 500px;
}
.wizard-nav-btn {
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.wizard-nav-btn.secondary {
background: transparent;
border: 1px solid var(--bp-border);
color: var(--bp-text-muted);
}
.wizard-nav-btn.secondary:hover {
background: var(--bp-surface);
}
.wizard-nav-btn.primary {
background: var(--bp-primary);
border: none;
color: white;
}
.wizard-nav-btn.primary:hover {
opacity: 0.9;
}
.wizard-nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ==========================================
INFO CARDS (Guided Inbox)
========================================== */
.guided-inbox {
display: none;
flex-direction: column;
height: 100%;
}
.guided-inbox.active {
display: flex;
}
/* Inbox Header */
.guided-inbox-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 32px;
border-bottom: 1px solid var(--bp-border);
background: var(--bp-surface);
}
.guided-inbox-title h2 {
font-size: 20px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 4px;
}
.guided-inbox-subtitle {
font-size: 14px;
color: var(--bp-text-muted);
}
.guided-inbox-actions {
display: flex;
gap: 12px;
}
/* Info Cards List */
.info-cards-container {
flex: 1;
overflow-y: auto;
padding: 24px 32px;
}
.info-cards-list {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 800px;
margin: 0 auto;
}
/* Single Info Card */
.info-card {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s;
}
.info-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
/* Card Header with Importance */
.info-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--bp-surface-elevated);
border-bottom: 1px solid var(--bp-border);
}
.info-card-importance {
display: flex;
align-items: center;
gap: 8px;
}
.importance-badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.importance-badge.kritisch {
background: #fef2f2;
color: #dc2626;
}
.importance-badge.dringend {
background: #fff7ed;
color: #ea580c;
}
.importance-badge.wichtig {
background: #fffbeb;
color: #d97706;
}
.importance-badge.pruefen {
background: #eff6ff;
color: #2563eb;
}
.importance-badge.info {
background: #f8fafc;
color: #64748b;
}
.info-card-time {
font-size: 12px;
color: var(--bp-text-muted);
}
/* Card Body */
.info-card-body {
padding: 16px;
}
.info-card-title {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 8px;
line-height: 1.4;
}
.info-card-source {
font-size: 13px;
color: var(--bp-primary);
margin-bottom: 12px;
}
/* Why Relevant Section */
.info-card-why {
background: var(--bp-surface-elevated);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.info-card-why-label {
font-size: 12px;
font-weight: 600;
color: var(--bp-text-muted);
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.info-card-why-content {
font-size: 14px;
color: var(--bp-text);
line-height: 1.5;
}
/* Next Steps Section */
.info-card-steps {
margin-top: 12px;
}
.info-card-steps-label {
font-size: 12px;
font-weight: 600;
color: var(--bp-text-muted);
margin-bottom: 8px;
}
.info-card-step {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: var(--bp-text);
margin-bottom: 6px;
}
.info-card-step-checkbox {
width: 16px;
height: 16px;
border: 2px solid var(--bp-border);
border-radius: 4px;
flex-shrink: 0;
margin-top: 2px;
}
/* Card Footer with Actions */
.info-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-top: 1px solid var(--bp-border);
background: var(--bp-surface-elevated);
}
.info-card-feedback {
display: flex;
gap: 8px;
}
.feedback-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--bp-border);
background: var(--bp-surface);
color: var(--bp-text-muted);
transition: all 0.2s;
}
.feedback-btn:hover {
background: var(--bp-surface-elevated);
}
.feedback-btn.negative:hover {
border-color: #ef4444;
color: #ef4444;
}
.feedback-btn.positive:hover {
border-color: var(--bp-success);
color: var(--bp-success);
}
.info-card-open {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--bp-primary);
color: white;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.info-card-open:hover {
opacity: 0.9;
}
/* Empty State */
.guided-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.guided-empty-icon {
font-size: 64px;
margin-bottom: 20px;
opacity: 0.5;
}
.guided-empty-title {
font-size: 20px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 8px;
}
.guided-empty-description {
font-size: 14px;
color: var(--bp-text-muted);
max-width: 400px;
}
/* ==========================================
DIGEST VIEW
========================================== */
.digest-container {
max-width: 800px;
margin: 0 auto;
padding: 24px;
}
.digest-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.digest-period {
font-size: 14px;
color: var(--bp-text-muted);
}
.digest-export-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 8px;
font-size: 14px;
color: var(--bp-text);
cursor: pointer;
}
.digest-export-btn:hover {
background: var(--bp-surface-elevated);
}
.digest-section {
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
}
.digest-section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--bp-surface-elevated);
border-bottom: 1px solid var(--bp-border);
}
.digest-section-title {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
}
.digest-section-count {
font-size: 14px;
color: var(--bp-text-muted);
}
.digest-items {
padding: 12px 20px;
}
.digest-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid var(--bp-border);
}
.digest-item:last-child {
border-bottom: none;
}
.digest-item-title {
font-size: 14px;
font-weight: 500;
color: var(--bp-text);
margin-bottom: 4px;
}
.digest-item-source {
font-size: 12px;
color: var(--bp-text-muted);
}
.digest-item-importance {
font-size: 11px;
padding: 4px 8px;
border-radius: 8px;
font-weight: 600;
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer den Guided Mode."""
return """
<!-- GUIDED MODE CONTAINER -->
<div class="guided-mode-container" id="guided-mode-container" style="display: none;">
<!-- Wizard -->
<div class="guided-wizard" id="guided-wizard">
<!-- Progress -->
<div class="wizard-progress">
<div class="wizard-step-indicator">
<div class="wizard-step-dot active" id="wizard-dot-1">1</div>
<div class="wizard-step-line" id="wizard-line-1"></div>
</div>
<div class="wizard-step-indicator">
<div class="wizard-step-dot" id="wizard-dot-2">2</div>
<div class="wizard-step-line" id="wizard-line-2"></div>
</div>
<div class="wizard-step-indicator">
<div class="wizard-step-dot" id="wizard-dot-3">3</div>
</div>
</div>
<!-- Step 1: Role Selection -->
<div class="wizard-step active" id="wizard-step-1">
<h2 class="wizard-step-title">Was beschreibt Sie am besten?</h2>
<p class="wizard-step-description">
Wir zeigen Ihnen passende Themen basierend auf Ihrer Rolle.
</p>
<div class="role-cards">
<div class="role-card" onclick="selectRole('lehrkraft')">
<div class="role-card-icon">&#128218;</div>
<div class="role-card-title">Ich unterrichte</div>
<div class="role-card-description">Lehrkraft mit Fokus auf Unterricht und Schueler</div>
</div>
<div class="role-card" onclick="selectRole('schulleitung')">
<div class="role-card-icon">&#127979;</div>
<div class="role-card-title">Ich leite die Schule</div>
<div class="role-card-description">Schulleitung, Verwaltung, Organisation</div>
</div>
<div class="role-card" onclick="selectRole('it_beauftragte')">
<div class="role-card-icon">&#128187;</div>
<div class="role-card-title">Ich bin IT-verantwortlich</div>
<div class="role-card-description">IT-Beauftragte, technischer Support</div>
</div>
</div>
<div class="wizard-nav">
<button class="wizard-nav-btn secondary" onclick="skipWizard()">Ueberspringen</button>
<button class="wizard-nav-btn primary" id="wizard-next-1" disabled onclick="goToWizardStep(2)">Weiter</button>
</div>
</div>
<!-- Step 2: Template Selection -->
<div class="wizard-step" id="wizard-step-2">
<h2 class="wizard-step-title">Welche Themen interessieren Sie?</h2>
<p class="wizard-step-description">
Waehlen Sie 1-3 Themen. Sie koennen diese spaeter anpassen.
</p>
<div class="template-grid" id="template-grid">
<!-- Wird per JavaScript befuellt -->
</div>
<div class="template-selection-info">
<span class="template-selection-count" id="template-count">0</span> von 3 Themen ausgewaehlt
</div>
<div class="wizard-nav">
<button class="wizard-nav-btn secondary" onclick="goToWizardStep(1)">Zurueck</button>
<button class="wizard-nav-btn primary" id="wizard-next-2" disabled onclick="goToWizardStep(3)">Weiter</button>
</div>
</div>
<!-- Step 3: Confirmation -->
<div class="wizard-step" id="wizard-step-3">
<h2 class="wizard-step-title">Fast geschafft!</h2>
<p class="wizard-step-description">
Pruefen Sie Ihre Auswahl und starten Sie.
</p>
<div class="confirmation-summary">
<div class="confirmation-item">
<span class="confirmation-label">Ihre Rolle</span>
<span class="confirmation-value" id="confirm-role">-</span>
</div>
<div class="confirmation-item">
<span class="confirmation-label">Ausgewaehlte Themen</span>
<div class="confirmation-templates" id="confirm-templates">
<!-- Wird per JavaScript befuellt -->
</div>
</div>
<div class="confirmation-item">
<span class="confirmation-label">Erwartete Meldungen</span>
<span class="confirmation-value">Ca. 5-10 pro Tag</span>
</div>
</div>
<div class="email-input-group">
<label class="email-input-label">E-Mail fuer Wochenzusammenfassung (optional)</label>
<p class="email-input-hint">Jeden Montag erhalten Sie eine Zusammenfassung per E-Mail.</p>
<input type="email" class="email-input" id="digest-email" placeholder="ihre.email@schule.de">
</div>
<div class="wizard-nav">
<button class="wizard-nav-btn secondary" onclick="goToWizardStep(2)">Zurueck</button>
<button class="wizard-nav-btn primary" onclick="completeWizard()">Jetzt starten</button>
</div>
</div>
</div>
<!-- Guided Inbox -->
<div class="guided-inbox" id="guided-inbox">
<div class="guided-inbox-header">
<div class="guided-inbox-title">
<h2>Ihre Meldungen</h2>
<p class="guided-inbox-subtitle">Aktuell <span id="guided-alert-count">0</span> relevante Meldungen</p>
</div>
<div class="guided-inbox-actions">
<button class="btn btn-ghost" onclick="showDigestView()">
&#128196; Wochenbericht
</button>
<button class="btn btn-ghost" onclick="openGuidedSettings()">
&#9881; Einstellungen
</button>
</div>
</div>
<div class="info-cards-container">
<div class="info-cards-list" id="info-cards-list">
<!-- Wird per JavaScript befuellt -->
</div>
<!-- Empty State -->
<div class="guided-empty-state" id="guided-empty-state" style="display: none;">
<div class="guided-empty-icon">&#127881;</div>
<h3 class="guided-empty-title">Keine neuen Meldungen</h3>
<p class="guided-empty-description">
Super! Sie sind auf dem neuesten Stand. Neue Meldungen erscheinen hier automatisch.
</p>
</div>
</div>
</div>
<!-- Digest Modal -->
<div class="modal-overlay" id="digest-modal" style="display: none;">
<div class="modal" style="max-width: 900px;">
<div class="modal-header">
<h3>Wochenbericht</h3>
<button class="modal-close" onclick="closeDigestModal()">&times;</button>
</div>
<div class="modal-body">
<div class="digest-container" id="digest-content">
<!-- Wird per JavaScript befuellt -->
</div>
</div>
</div>
</div>
</div>
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer den Guided Mode."""
return """
/* ==========================================
GUIDED MODE - STATE & CONFIG
========================================== */
const GUIDED_API_BASE = '/api/alerts';
let guidedState = {
mode: 'guided', // 'guided' oder 'expert'
wizardCompleted: false,
wizardStep: 1,
selectedRole: null,
selectedTemplates: [],
templates: [],
infoCards: [],
digestEmail: ''
};
/* ==========================================
MODE SWITCHING
========================================== */
function switchToGuidedMode() {
guidedState.mode = 'guided';
document.getElementById('guided-mode-container').style.display = 'block';
document.getElementById('panel-alerts').querySelector('.alerts-content').style.display = 'none';
// Update mode buttons
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector('.mode-btn[data-mode="guided"]').classList.add('active');
// Check wizard state
checkWizardState();
}
function switchToExpertMode() {
guidedState.mode = 'expert';
document.getElementById('guided-mode-container').style.display = 'none';
document.getElementById('panel-alerts').querySelector('.alerts-content').style.display = 'block';
// Update mode buttons
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector('.mode-btn[data-mode="expert"]').classList.add('active');
}
async function checkWizardState() {
try {
const response = await fetch(`${GUIDED_API_BASE}/wizard/state`);
const data = await response.json();
guidedState.wizardCompleted = data.wizard_completed;
guidedState.selectedRole = data.user_role;
guidedState.selectedTemplates = data.selected_template_ids || [];
if (data.wizard_completed) {
showGuidedInbox();
} else {
showWizard();
}
} catch (error) {
console.log('Demo mode: wizard state check');
showWizard();
}
}
/* ==========================================
WIZARD FUNCTIONS
========================================== */
function showWizard() {
document.getElementById('guided-wizard').classList.add('active');
document.getElementById('guided-inbox').classList.remove('active');
loadTemplatesForWizard();
}
function showGuidedInbox() {
document.getElementById('guided-wizard').classList.remove('active');
document.getElementById('guided-inbox').classList.add('active');
loadInfoCards();
}
function goToWizardStep(step) {
// Update step visibility
for (let i = 1; i <= 3; i++) {
document.getElementById(`wizard-step-${i}`).classList.remove('active');
document.getElementById(`wizard-dot-${i}`).classList.remove('active', 'completed');
if (i < 3) {
document.getElementById(`wizard-line-${i}`).classList.remove('completed');
}
}
document.getElementById(`wizard-step-${step}`).classList.add('active');
// Update progress indicators
for (let i = 1; i <= step; i++) {
if (i < step) {
document.getElementById(`wizard-dot-${i}`).classList.add('completed');
if (i < 3) {
document.getElementById(`wizard-line-${i}`).classList.add('completed');
}
} else {
document.getElementById(`wizard-dot-${i}`).classList.add('active');
}
}
guidedState.wizardStep = step;
// Update confirmation if on step 3
if (step === 3) {
updateConfirmation();
}
}
function selectRole(role) {
guidedState.selectedRole = role;
// Update UI
document.querySelectorAll('.role-card').forEach(card => card.classList.remove('selected'));
event.currentTarget.classList.add('selected');
// Enable next button
document.getElementById('wizard-next-1').disabled = false;
// Update template recommendations
filterTemplatesByRole(role);
}
async function loadTemplatesForWizard() {
try {
const response = await fetch(`${GUIDED_API_BASE}/templates`);
const data = await response.json();
guidedState.templates = data.templates || [];
renderTemplateGrid();
} catch (error) {
console.log('Demo mode: loading templates');
guidedState.templates = getDemoTemplates();
renderTemplateGrid();
}
}
function getDemoTemplates() {
return [
{ id: '1', slug: 'foerderprogramme', name: 'Foerderprogramme & Fristen', icon: '&#128176;', description: 'Foerdergelder, Antragsfristen, EU-Programme', target_roles: ['schulleitung'] },
{ id: '2', slug: 'abitur-updates', name: 'Abitur-Updates', icon: '&#128221;', description: 'Pruefungstermine, Operatoren, KMK-Beschluesse', target_roles: ['lehrkraft'] },
{ id: '3', slug: 'fortbildungen', name: 'Fortbildungen', icon: '&#127891;', description: 'Seminare, Workshops, Online-Kurse', target_roles: ['lehrkraft'] },
{ id: '4', slug: 'datenschutz-recht', name: 'Datenschutz & Recht', icon: '&#9878;', description: 'DSGVO-Updates, Urteile, Handreichungen', target_roles: ['schulleitung', 'it_beauftragte'] },
{ id: '5', slug: 'it-security', name: 'IT-Security', icon: '&#128274;', description: 'Sicherheitsluecken, Phishing-Warnungen', target_roles: ['it_beauftragte'] },
{ id: '6', slug: 'wettbewerbe-projekte', name: 'Wettbewerbe & Projekte', icon: '&#127942;', description: 'Schueler-Wettbewerbe, Projekttage', target_roles: ['lehrkraft'] }
];
}
function renderTemplateGrid() {
const grid = document.getElementById('template-grid');
grid.innerHTML = guidedState.templates.map(template => `
<div class="template-card ${guidedState.selectedTemplates.includes(template.id) ? 'selected' : ''}"
data-id="${template.id}"
onclick="toggleTemplate('${template.id}')">
<div class="template-card-check">${guidedState.selectedTemplates.includes(template.id) ? '&#10003;' : ''}</div>
<div class="template-card-header">
<div class="template-card-icon">${template.icon}</div>
<div class="template-card-info">
<div class="template-card-name">${template.name}</div>
<div class="template-card-desc">${template.description}</div>
</div>
</div>
</div>
`).join('');
}
function filterTemplatesByRole(role) {
// Highlight recommended templates for role
document.querySelectorAll('.template-card').forEach(card => {
const id = card.dataset.id;
const template = guidedState.templates.find(t => t.id === id);
if (template && template.target_roles && template.target_roles.includes(role)) {
card.style.order = '-1';
} else {
card.style.order = '0';
}
});
}
function toggleTemplate(templateId) {
const index = guidedState.selectedTemplates.indexOf(templateId);
if (index > -1) {
guidedState.selectedTemplates.splice(index, 1);
} else if (guidedState.selectedTemplates.length < 3) {
guidedState.selectedTemplates.push(templateId);
} else {
alert('Maximal 3 Themen auswaehlbar. Entfernen Sie erst ein Thema.');
return;
}
// Update UI
renderTemplateGrid();
updateTemplateCount();
// Enable/disable next button
document.getElementById('wizard-next-2').disabled = guidedState.selectedTemplates.length === 0;
}
function updateTemplateCount() {
document.getElementById('template-count').textContent = guidedState.selectedTemplates.length;
}
function updateConfirmation() {
// Role
const roleLabels = {
'lehrkraft': 'Lehrkraft',
'schulleitung': 'Schulleitung',
'it_beauftragte': 'IT-Beauftragte/r'
};
document.getElementById('confirm-role').textContent = roleLabels[guidedState.selectedRole] || '-';
// Templates
const templatesContainer = document.getElementById('confirm-templates');
templatesContainer.innerHTML = guidedState.selectedTemplates.map(id => {
const template = guidedState.templates.find(t => t.id === id);
return `<span class="confirmation-template-tag">${template ? template.name : id}</span>`;
}).join('');
}
async function completeWizard() {
const email = document.getElementById('digest-email').value;
guidedState.digestEmail = email;
try {
// Step 1 speichern
await fetch(`${GUIDED_API_BASE}/wizard/step/1`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: guidedState.selectedRole })
});
// Step 2 speichern
await fetch(`${GUIDED_API_BASE}/wizard/step/2`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ template_ids: guidedState.selectedTemplates })
});
// Step 3 speichern
await fetch(`${GUIDED_API_BASE}/wizard/step/3`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notification_email: email })
});
// Wizard abschliessen
await fetch(`${GUIDED_API_BASE}/wizard/complete`, {
method: 'POST'
});
} catch (error) {
console.log('Demo mode: completing wizard');
}
guidedState.wizardCompleted = true;
showGuidedInbox();
}
function skipWizard() {
if (confirm('Moechten Sie wirklich ueberspringen? Sie koennen die Einstellungen spaeter anpassen.')) {
switchToExpertMode();
}
}
/* ==========================================
INFO CARDS (Guided Inbox)
========================================== */
async function loadInfoCards() {
try {
const response = await fetch(`${GUIDED_API_BASE}/inbox/guided?limit=10`);
const data = await response.json();
guidedState.infoCards = data.items || [];
renderInfoCards();
} catch (error) {
console.log('Demo mode: loading info cards');
guidedState.infoCards = getDemoInfoCards();
renderInfoCards();
}
}
function getDemoInfoCards() {
return [
{
id: '1',
title: 'DigitalPakt 2.0: Neue Antragsphase startet am 1. April',
source_name: 'Bundesministerium fuer Bildung',
source_url: 'https://example.com/digitalpakt',
importance_level: 'dringend',
why_relevant: 'Frist endet in 45 Tagen. Betrifft alle Schulen mit Foerderbedarf.',
next_steps: ['Schultraeger kontaktieren', 'Bedarfsanalyse erstellen'],
fetched_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()
},
{
id: '2',
title: 'CVE-2026-1234: Kritische Sicherheitsluecke in Moodle',
source_name: 'BSI CERT-Bund',
source_url: 'https://example.com/cve',
importance_level: 'kritisch',
why_relevant: 'Sofortiges Update erforderlich. Exploit bereits aktiv.',
next_steps: ['Betroffene Systeme pruefen', 'Update einspielen'],
fetched_at: new Date(Date.now() - 30 * 60 * 1000).toISOString()
},
{
id: '3',
title: 'Kostenlose Fortbildung: KI im Unterricht',
source_name: 'Landesinstitut',
source_url: 'https://example.com/fortbildung',
importance_level: 'pruefen',
why_relevant: 'Passt zu Ihrem Interessenprofil. Online-Format, 4 Stunden.',
next_steps: ['Termin und Ort pruefen', 'Bei Interesse anmelden'],
fetched_at: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
}
];
}
function renderInfoCards() {
const list = document.getElementById('info-cards-list');
const emptyState = document.getElementById('guided-empty-state');
if (guidedState.infoCards.length === 0) {
list.style.display = 'none';
emptyState.style.display = 'flex';
document.getElementById('guided-alert-count').textContent = '0';
return;
}
list.style.display = 'flex';
emptyState.style.display = 'none';
document.getElementById('guided-alert-count').textContent = guidedState.infoCards.length;
list.innerHTML = guidedState.infoCards.map(card => {
const timeAgo = formatTimeAgo(card.fetched_at);
const importanceClass = (card.importance_level || 'info').toLowerCase();
const importanceLabel = {
'kritisch': 'Kritisch',
'dringend': 'Dringend',
'wichtig': 'Wichtig',
'pruefen': 'Zu pruefen',
'info': 'Info'
}[importanceClass] || 'Info';
const stepsHtml = (card.next_steps || []).map(step => `
<div class="info-card-step">
<div class="info-card-step-checkbox"></div>
<span>${escapeHtml(step)}</span>
</div>
`).join('');
return `
<div class="info-card" data-id="${card.id}">
<div class="info-card-header">
<div class="info-card-importance">
<span class="importance-badge ${importanceClass}">${importanceLabel}</span>
</div>
<span class="info-card-time">${timeAgo}</span>
</div>
<div class="info-card-body">
<div class="info-card-title">${escapeHtml(card.title)}</div>
<div class="info-card-source">${escapeHtml(card.source_name || 'Unbekannte Quelle')}</div>
<div class="info-card-why">
<div class="info-card-why-label">
&#128161; Warum relevant?
</div>
<div class="info-card-why-content">${escapeHtml(card.why_relevant || '')}</div>
</div>
${stepsHtml ? `
<div class="info-card-steps">
<div class="info-card-steps-label">Naechste Schritte:</div>
${stepsHtml}
</div>
` : ''}
</div>
<div class="info-card-footer">
<div class="info-card-feedback">
<button class="feedback-btn negative" onclick="sendQuickFeedback('${card.id}', 'not_relevant')">
&#128078; Nicht relevant
</button>
<button class="feedback-btn positive" onclick="sendQuickFeedback('${card.id}', 'more_like_this')">
&#128077; Mehr davon
</button>
</div>
<button class="info-card-open" onclick="openInfoCard('${card.id}', '${escapeHtml(card.source_url || '')}')">
Oeffnen &#8594;
</button>
</div>
</div>
`;
}).join('');
}
function openInfoCard(cardId, url) {
if (url) {
window.open(url, '_blank');
}
}
async function sendQuickFeedback(cardId, feedbackType) {
try {
await fetch(`${GUIDED_API_BASE}/feedback/quick`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
alert_id: cardId,
feedback_type: feedbackType
})
});
} catch (error) {
console.log('Demo mode: sending feedback');
}
// Remove card from list
guidedState.infoCards = guidedState.infoCards.filter(c => c.id !== cardId);
renderInfoCards();
// Show confirmation
const message = feedbackType === 'more_like_this'
? 'Danke! Sie erhalten mehr aehnliche Meldungen.'
: 'Verstanden! Weniger solche Meldungen.';
showToast(message);
}
function showToast(message) {
// Simple toast notification
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #1e293b;
color: white;
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 10000;
animation: fadeIn 0.3s, fadeOut 0.3s 2.7s;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
/* ==========================================
DIGEST VIEW
========================================== */
async function showDigestView() {
document.getElementById('digest-modal').style.display = 'flex';
await loadDigest();
}
function closeDigestModal() {
document.getElementById('digest-modal').style.display = 'none';
}
async function loadDigest() {
const container = document.getElementById('digest-content');
try {
const response = await fetch(`${GUIDED_API_BASE}/digests?limit=1`);
const data = await response.json();
if (data.digests && data.digests.length > 0) {
renderDigest(data.digests[0]);
} else {
container.innerHTML = '<p style="text-align: center; color: var(--bp-text-muted);">Noch kein Wochenbericht verfuegbar.</p>';
}
} catch (error) {
console.log('Demo mode: loading digest');
renderDemoDigest();
}
}
function renderDemoDigest() {
const container = document.getElementById('digest-content');
container.innerHTML = `
<div class="digest-header">
<div>
<h2 style="margin: 0 0 4px 0;">Wochenbericht</h2>
<div class="digest-period">13.01.2026 - 19.01.2026</div>
</div>
<button class="digest-export-btn" onclick="exportDigestPDF()">
&#128196; Als PDF exportieren
</button>
</div>
<div class="digest-section">
<div class="digest-section-header">
<div class="digest-section-title">&#128308; Kritisch & Dringend</div>
<div class="digest-section-count">2 Meldungen</div>
</div>
<div class="digest-items">
<div class="digest-item">
<div>
<div class="digest-item-title">CVE-2026-1234: Moodle Sicherheitsluecke</div>
<div class="digest-item-source">BSI CERT-Bund</div>
</div>
<span class="digest-item-importance" style="background: #fef2f2; color: #dc2626;">Kritisch</span>
</div>
<div class="digest-item">
<div>
<div class="digest-item-title">DigitalPakt 2.0 Antragsphase</div>
<div class="digest-item-source">BMBF</div>
</div>
<span class="digest-item-importance" style="background: #fff7ed; color: #ea580c;">Dringend</span>
</div>
</div>
</div>
<div class="digest-section">
<div class="digest-section-header">
<div class="digest-section-title">&#128994; Wichtig</div>
<div class="digest-section-count">3 Meldungen</div>
</div>
<div class="digest-items">
<div class="digest-item">
<div>
<div class="digest-item-title">Neue Operatoren fuer Abitur Deutsch</div>
<div class="digest-item-source">KMK</div>
</div>
<span class="digest-item-importance" style="background: #fffbeb; color: #d97706;">Wichtig</span>
</div>
<div class="digest-item">
<div>
<div class="digest-item-title">Fortbildung: KI im Unterricht</div>
<div class="digest-item-source">Landesinstitut</div>
</div>
<span class="digest-item-importance" style="background: #eff6ff; color: #2563eb;">Pruefen</span>
</div>
<div class="digest-item">
<div>
<div class="digest-item-title">Jugend forscht Anmeldeschluss</div>
<div class="digest-item-source">Jugend forscht e.V.</div>
</div>
<span class="digest-item-importance" style="background: #fffbeb; color: #d97706;">Wichtig</span>
</div>
</div>
</div>
`;
}
function renderDigest(digest) {
const container = document.getElementById('digest-content');
container.innerHTML = digest.summary_html || '<p>Kein Inhalt verfuegbar.</p>';
}
async function exportDigestPDF() {
try {
const response = await fetch(`${GUIDED_API_BASE}/digests/latest/pdf`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'wochenbericht.pdf';
a.click();
} catch (error) {
console.log('Demo mode: PDF export not available');
alert('PDF-Export wird in Kuerze verfuegbar sein.');
}
}
/* ==========================================
SETTINGS
========================================== */
function openGuidedSettings() {
// Zurueck zum Wizard um Einstellungen anzupassen
guidedState.wizardCompleted = false;
showWizard();
}
/* ==========================================
INITIALIZATION
========================================== */
// Add mode switcher to alerts header on load
document.addEventListener('DOMContentLoaded', function() {
const headerActions = document.querySelector('.alerts-header-actions');
if (headerActions) {
const modeSwitcher = document.createElement('div');
modeSwitcher.className = 'alerts-mode-switcher';
modeSwitcher.innerHTML = `
<button class="mode-btn active" data-mode="guided" onclick="switchToGuidedMode()">Einfach</button>
<button class="mode-btn" data-mode="expert" onclick="switchToExpertMode()">Experte</button>
`;
headerActions.insertBefore(modeSwitcher, headerActions.firstChild);
}
// Default to guided mode for new users
setTimeout(() => {
if (document.getElementById('panel-alerts').classList.contains('active')) {
switchToGuidedMode();
}
}, 100);
});
// Override showAlertsPanel to check mode
const originalShowAlertsTab = window.showAlertsTab;
window.showAlertsTab = function(tab) {
if (guidedState.mode === 'guided') {
// Stay in guided mode
return;
}
if (originalShowAlertsTab) {
originalShowAlertsTab(tab);
}
};
"""