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
Benjamin Admin 21a844cb8a 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

1108 lines
37 KiB
Python

"""
Alerts Module - JavaScript.
Enthält die JavaScript-Logik für das Alerts Agent Modul:
- Inbox management
- Topic management
- Rule builder
- Alert actions
"""
def get_alerts_js() -> str:
"""JavaScript fuer das Alerts-Modul."""
return """
/* ==========================================
ALERTS MODULE - JavaScript
========================================== */
// API Base URL
const ALERTS_API_BASE = '/api/alerts';
// State
let alertsData = {
items: [],
topics: [],
rules: [],
profile: null,
currentFilter: 'all',
searchQuery: ''
};
/* ==========================================
INITIALIZATION
========================================== */
function loadAlertsModule() {
console.log('Alerts Module loaded');
loadAlertsData();
}
async function loadAlertsData() {
await Promise.all([
loadAlerts(),
loadTopics(),
loadRules(),
loadProfile()
]);
updateAlertsStats();
}
/* ==========================================
TAB NAVIGATION
========================================== */
function showAlertsTab(tabId) {
// Update tab buttons
document.querySelectorAll('.alerts-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab panels
document.querySelectorAll('.alerts-tab-panel').forEach(panel => {
panel.classList.remove('active');
});
document.getElementById('alerts-panel-' + tabId).classList.add('active');
}
/* ==========================================
ALERTS INBOX
========================================== */
async function loadAlerts() {
try {
const response = await fetch(`${ALERTS_API_BASE}/inbox?limit=100&status=${alertsData.currentFilter === 'all' ? '' : alertsData.currentFilter}`);
if (!response.ok) throw new Error('API not available');
const data = await response.json();
alertsData.items = Array.isArray(data.items) ? data.items : (Array.isArray(data) ? data : []);
renderAlerts();
} catch (error) {
console.log('Alerts API not available, using demo data');
// Demo data
alertsData.items = [
{
id: 'alert_1',
title: 'Neue Studie zur digitalen Bildung an Schulen',
url: 'https://example.com/artikel1',
snippet: 'Eine aktuelle Studie zeigt, dass digitale Lernmittel den Lernerfolg steigern koennen...',
topic_name: 'Digitale Bildung',
relevance_score: 0.85,
relevance_decision: 'KEEP',
status: 'new',
fetched_at: new Date().toISOString()
},
{
id: 'alert_2',
title: 'Inklusion: Fortbildungen fuer Lehrkraefte',
url: 'https://example.com/artikel2',
snippet: 'Das Kultusministerium bietet neue Fortbildungsangebote zum Thema Inklusion an...',
topic_name: 'Inklusion',
relevance_score: 0.72,
relevance_decision: 'KEEP',
status: 'new',
fetched_at: new Date(Date.now() - 3600000).toISOString()
},
{
id: 'alert_3',
title: 'Stellenangebot: Schulleitung gesucht',
url: 'https://example.com/job',
snippet: 'Wir suchen eine engagierte Schulleitung fuer unsere Grundschule...',
topic_name: 'Schule allgemein',
relevance_score: 0.25,
relevance_decision: 'DROP',
status: 'archived',
fetched_at: new Date(Date.now() - 7200000).toISOString()
}
];
renderAlerts();
}
}
function renderAlerts() {
const list = document.getElementById('alerts-list');
const emptyState = document.getElementById('alerts-empty-state');
// Filter alerts
let filtered = alertsData.items;
if (alertsData.currentFilter !== 'all') {
if (alertsData.currentFilter === 'new') {
filtered = filtered.filter(a => a.status === 'new');
} else if (alertsData.currentFilter === 'keep') {
filtered = filtered.filter(a => a.relevance_decision === 'KEEP');
} else if (alertsData.currentFilter === 'review') {
filtered = filtered.filter(a => a.relevance_decision === 'REVIEW');
}
}
// Search filter
if (alertsData.searchQuery) {
const q = alertsData.searchQuery.toLowerCase();
filtered = filtered.filter(a =>
a.title.toLowerCase().includes(q) ||
a.snippet.toLowerCase().includes(q)
);
}
if (filtered.length === 0) {
list.style.display = 'none';
emptyState.style.display = 'flex';
return;
}
list.style.display = 'flex';
emptyState.style.display = 'none';
list.innerHTML = filtered.map(alert => {
const score = alert.relevance_score || 0;
const scoreClass = score >= 0.7 ? 'high' : score >= 0.4 ? 'medium' : 'low';
const decisionClass = (alert.relevance_decision || '').toLowerCase();
const timeAgo = formatTimeAgo(alert.fetched_at);
return `
<div class="alert-item ${alert.status === 'new' ? 'unread' : ''}" onclick="openAlert('${alert.id}')">
<div class="alert-item-checkbox" onclick="event.stopPropagation()">
<input type="checkbox" data-id="${alert.id}">
</div>
<div class="alert-item-content">
<div class="alert-item-header">
<div>
<div class="alert-item-topic">${alert.topic_name || 'Unbekannt'}</div>
<div class="alert-item-title">${escapeHtml(alert.title)}</div>
</div>
</div>
<div class="alert-item-snippet">${escapeHtml(alert.snippet || '')}</div>
<div class="alert-item-meta">
<span class="alert-item-score ${scoreClass}">${Math.round(score * 100)}% Relevanz</span>
<span class="alert-item-decision ${decisionClass}">${alert.relevance_decision || '-'}</span>
</div>
</div>
<div class="alert-item-actions">
<span class="alert-item-time">${timeAgo}</span>
<div class="alert-item-action-btns">
<button class="alert-action-btn keep" onclick="event.stopPropagation(); markAlertKeep('${alert.id}')" title="Relevant">&#10003;</button>
<button class="alert-action-btn drop" onclick="event.stopPropagation(); markAlertDrop('${alert.id}')" title="Verwerfen">&#10007;</button>
</div>
</div>
</div>
`;
}).join('');
}
function filterAlerts(filter) {
alertsData.currentFilter = filter;
// Update filter buttons
document.querySelectorAll('.alerts-filter-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
renderAlerts();
}
function searchAlerts(query) {
alertsData.searchQuery = query;
renderAlerts();
}
function openAlert(alertId) {
const alert = alertsData.items.find(a => a.id === alertId);
if (alert && alert.url) {
window.open(alert.url, '_blank');
// Mark as read
markAlertRead(alertId);
}
}
async function markAlertKeep(alertId) {
try {
await fetch(`${ALERTS_API_BASE}/inbox/${alertId}/action`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'keep' })
});
} catch (error) {
console.log('Demo mode: marking as keep');
}
const alert = alertsData.items.find(a => a.id === alertId);
if (alert) {
alert.relevance_decision = 'KEEP';
alert.status = 'kept';
}
renderAlerts();
updateAlertsStats();
}
async function markAlertDrop(alertId) {
try {
await fetch(`${ALERTS_API_BASE}/inbox/${alertId}/action`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'archive' })
});
} catch (error) {
console.log('Demo mode: marking as drop');
}
const alert = alertsData.items.find(a => a.id === alertId);
if (alert) {
alert.relevance_decision = 'DROP';
alert.status = 'archived';
}
renderAlerts();
updateAlertsStats();
}
async function markAlertRead(alertId) {
const alert = alertsData.items.find(a => a.id === alertId);
if (alert) {
alert.status = 'read';
}
renderAlerts();
updateAlertsStats();
}
/* ==========================================
TOPICS
========================================== */
async function loadTopics() {
try {
const response = await fetch(`${ALERTS_API_BASE}/topics`);
if (!response.ok) throw new Error('API not available');
const data = await response.json();
alertsData.topics = Array.isArray(data.items) ? data.items : (Array.isArray(data) ? data : []);
renderTopics();
} catch (error) {
console.log('Topics API not available, using demo data');
// Demo data
alertsData.topics = [
{
id: 'topic_1',
name: 'Digitale Bildung',
feed_url: 'https://www.google.com/alerts/feeds/123',
feed_type: 'rss',
is_active: true,
fetch_interval_minutes: 60,
alert_count: 47,
last_fetched_at: new Date().toISOString()
},
{
id: 'topic_2',
name: 'Inklusion',
feed_url: 'https://www.google.com/alerts/feeds/456',
feed_type: 'rss',
is_active: true,
fetch_interval_minutes: 60,
alert_count: 32,
last_fetched_at: new Date(Date.now() - 1800000).toISOString()
}
];
renderTopics();
}
}
function renderTopics() {
const grid = document.getElementById('topics-grid');
grid.innerHTML = alertsData.topics.map(topic => `
<div class="topic-card">
<div class="topic-card-header">
<div class="topic-card-icon">&#128240;</div>
<span class="topic-card-status ${topic.is_active ? 'active' : 'paused'}">
${topic.is_active ? 'Aktiv' : 'Pausiert'}
</span>
</div>
<div class="topic-card-name">${escapeHtml(topic.name)}</div>
<div class="topic-card-url">${escapeHtml(topic.feed_url || '')}</div>
<div class="topic-card-stats">
<div class="topic-card-stat">
<div class="topic-card-stat-value">${topic.alert_count || 0}</div>
<div class="topic-card-stat-label">Alerts</div>
</div>
<div class="topic-card-stat">
<div class="topic-card-stat-value">${topic.fetch_interval_minutes || 60}m</div>
<div class="topic-card-stat-label">Intervall</div>
</div>
</div>
<div class="topic-card-actions">
<button class="topic-card-btn topic-card-btn-primary" onclick="fetchTopic('${topic.id}')">
&#8635; Abrufen
</button>
<button class="topic-card-btn topic-card-btn-secondary" onclick="editTopic('${topic.id}')">
&#9998; Bearbeiten
</button>
</div>
</div>
`).join('') + `
<div class="topic-card topic-card-add" onclick="openAddTopicModal()">
<div class="topic-card-add-icon">&#10133;</div>
<div class="topic-card-add-text">Topic hinzufuegen</div>
</div>
`;
}
function openAddTopicModal() {
document.getElementById('topic-name').value = '';
document.getElementById('topic-feed-url').value = '';
document.getElementById('topic-feed-type').value = 'rss';
document.getElementById('topic-interval').value = '60';
document.getElementById('add-topic-modal').classList.add('active');
}
function closeAddTopicModal() {
document.getElementById('add-topic-modal').classList.remove('active');
}
async function saveTopic() {
const name = document.getElementById('topic-name').value;
const feedUrl = document.getElementById('topic-feed-url').value;
const feedType = document.getElementById('topic-feed-type').value;
const interval = parseInt(document.getElementById('topic-interval').value);
if (!name || !feedUrl) {
alert('Bitte Name und Feed-URL eingeben');
return;
}
try {
await fetch(`${ALERTS_API_BASE}/topics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
feed_url: feedUrl,
feed_type: feedType,
fetch_interval_minutes: interval
})
});
} catch (error) {
console.log('Demo mode: saving topic');
alertsData.topics.push({
id: 'topic_' + Date.now(),
name,
feed_url: feedUrl,
feed_type: feedType,
is_active: true,
fetch_interval_minutes: interval,
alert_count: 0
});
}
closeAddTopicModal();
await loadTopics();
updateAlertsStats();
}
async function fetchTopic(topicId) {
try {
await fetch(`${ALERTS_API_BASE}/topics/${topicId}/fetch`, { method: 'POST' });
alert('Feed wird abgerufen...');
await loadAlerts();
} catch (error) {
console.log('Demo mode: fetching topic');
alert('Feed wird abgerufen... (Demo)');
}
}
function editTopic(topicId) {
alert('Topic bearbeiten: ' + topicId + ' (nicht implementiert in Demo)');
}
/* ==========================================
RULES
========================================== */
async function loadRules() {
try {
const response = await fetch(`${ALERTS_API_BASE}/rules`);
if (!response.ok) throw new Error('API not available');
const data = await response.json();
alertsData.rules = Array.isArray(data.items) ? data.items : (Array.isArray(data) ? data : []);
renderRules();
} catch (error) {
console.log('Rules API not available, using demo data');
// Demo data
alertsData.rules = [
{
id: 'rule_1',
name: 'Stellenanzeigen ausschliessen',
conditions: [{ field: 'title', operator: 'contains', value: 'Stellenangebot' }],
action_type: 'drop',
is_active: true,
priority: 10
},
{
id: 'rule_2',
name: 'Inklusion priorisieren',
conditions: [{ field: 'title', operator: 'contains', value: 'Inklusion' }],
action_type: 'keep',
is_active: true,
priority: 5
}
];
renderRules();
}
}
function renderRules() {
const list = document.getElementById('rules-list');
if (alertsData.rules.length === 0) {
list.innerHTML = '<div class="alerts-empty-state"><div class="alerts-empty-icon">&#128203;</div><h3 class="alerts-empty-title">Keine Regeln</h3><p class="alerts-empty-description">Erstellen Sie Regeln zur automatischen Filterung.</p></div>';
return;
}
list.innerHTML = alertsData.rules.map(rule => {
const condition = rule.conditions && rule.conditions[0];
const conditionText = condition
? `${condition.field} ${condition.operator} "${condition.value}"`
: 'Keine Bedingung';
return `
<div class="rule-item">
<div class="rule-item-drag">&#9776;</div>
<div class="rule-item-content">
<div class="rule-item-name">${escapeHtml(rule.name)}</div>
<div class="rule-item-conditions">Wenn: ${conditionText}</div>
</div>
<span class="rule-item-action ${rule.action_type}">${rule.action_type.toUpperCase()}</span>
<div class="rule-item-toggle ${rule.is_active ? 'active' : ''}" onclick="toggleRule('${rule.id}')"></div>
</div>
`;
}).join('');
}
function openAddRuleModal() {
document.getElementById('rule-name').value = '';
document.getElementById('rule-field').value = 'title';
document.getElementById('rule-operator').value = 'contains';
document.getElementById('rule-value').value = '';
document.getElementById('rule-action').value = 'drop';
document.getElementById('add-rule-modal').classList.add('active');
}
function closeAddRuleModal() {
document.getElementById('add-rule-modal').classList.remove('active');
}
async function saveRule() {
const name = document.getElementById('rule-name').value;
const field = document.getElementById('rule-field').value;
const operator = document.getElementById('rule-operator').value;
const value = document.getElementById('rule-value').value;
const action = document.getElementById('rule-action').value;
if (!name || !value) {
alert('Bitte Name und Wert eingeben');
return;
}
try {
await fetch(`${ALERTS_API_BASE}/rules`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
conditions: [{ field, operator, value }],
action_type: action,
is_active: true
})
});
} catch (error) {
console.log('Demo mode: saving rule');
alertsData.rules.push({
id: 'rule_' + Date.now(),
name,
conditions: [{ field, operator, value }],
action_type: action,
is_active: true,
priority: alertsData.rules.length + 1
});
}
closeAddRuleModal();
await loadRules();
}
async function toggleRule(ruleId) {
const rule = alertsData.rules.find(r => r.id === ruleId);
if (rule) {
rule.is_active = !rule.is_active;
renderRules();
try {
await fetch(`${ALERTS_API_BASE}/rules/${ruleId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rule)
});
} catch (error) {
console.log('Demo mode: toggling rule');
}
}
}
/* ==========================================
PROFILE
========================================== */
async function loadProfile() {
try {
const response = await fetch(`${ALERTS_API_BASE}/profile`);
alertsData.profile = await response.json();
renderProfile();
} catch (error) {
console.error('Error loading profile:', error);
alertsData.profile = {
priorities: ['Inklusion', 'digitale Bildung'],
exclusions: ['Stellenanzeigen', 'Werbung'],
policies: { keep_threshold: 0.7, drop_threshold: 0.3 }
};
renderProfile();
}
}
function renderProfile() {
if (!alertsData.profile) return;
const priorities = alertsData.profile.priorities || [];
const exclusions = alertsData.profile.exclusions || [];
const policies = alertsData.profile.policies || {};
document.getElementById('profile-priorities').value = priorities.join('\\n');
document.getElementById('profile-exclusions').value = exclusions.join('\\n');
document.getElementById('profile-keep-threshold').value = policies.keep_threshold || 0.7;
document.getElementById('profile-drop-threshold').value = policies.drop_threshold || 0.3;
}
async function saveProfile() {
const priorities = document.getElementById('profile-priorities').value
.split('\\n')
.map(s => s.trim())
.filter(s => s);
const exclusions = document.getElementById('profile-exclusions').value
.split('\\n')
.map(s => s.trim())
.filter(s => s);
const keepThreshold = parseFloat(document.getElementById('profile-keep-threshold').value);
const dropThreshold = parseFloat(document.getElementById('profile-drop-threshold').value);
try {
await fetch(`${ALERTS_API_BASE}/profile`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
priorities,
exclusions,
policies: {
keep_threshold: keepThreshold,
drop_threshold: dropThreshold
}
})
});
alert('Profil gespeichert!');
} catch (error) {
console.log('Demo mode: saving profile');
alertsData.profile = { priorities, exclusions, policies: { keep_threshold: keepThreshold, drop_threshold: dropThreshold } };
alert('Profil gespeichert! (Demo)');
}
}
/* ==========================================
SYNC & STATS
========================================== */
async function syncAllAlerts() {
alert('Synchronisierung gestartet...');
await loadAlertsData();
}
function updateAlertsStats() {
const newCount = alertsData.items.filter(a => a.status === 'new').length;
const keepCount = alertsData.items.filter(a => a.relevance_decision === 'KEEP').length;
const reviewCount = alertsData.items.filter(a => a.relevance_decision === 'REVIEW').length;
const topicsCount = alertsData.topics.length;
document.getElementById('alerts-stat-new').textContent = newCount;
document.getElementById('alerts-stat-keep').textContent = keepCount;
document.getElementById('alerts-stat-review').textContent = reviewCount;
document.getElementById('alerts-stat-topics').textContent = topicsCount;
document.getElementById('alerts-inbox-badge').textContent = newCount;
}
/* ==========================================
UTILITIES
========================================== */
function formatTimeAgo(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'gerade eben';
if (diffMins < 60) return `vor ${diffMins} Min.`;
if (diffMins < 1440) return `vor ${Math.floor(diffMins / 60)} Std.`;
return `vor ${Math.floor(diffMins / 1440)} Tagen`;
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Register module for panel loading
if (typeof PANEL_IDS !== 'undefined') {
PANEL_IDS.push('panel-alerts');
}
/* ==========================================
GUIDED MODE - STATE & FUNCTIONS
========================================== */
let guidedState = {
mode: 'guided',
wizardCompleted: false,
wizardStep: 1,
selectedRole: null,
selectedTemplates: [],
templates: [],
infoCards: []
};
function switchToGuidedMode() {
guidedState.mode = 'guided';
document.getElementById('guided-mode-container').style.display = 'flex';
document.getElementById('expert-mode-container').style.display = 'none';
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector('.mode-btn[data-mode="guided"]').classList.add('active');
checkWizardState();
}
function switchToExpertMode() {
guidedState.mode = 'expert';
document.getElementById('guided-mode-container').style.display = 'none';
document.getElementById('expert-mode-container').style.display = 'block';
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(`${ALERTS_API_BASE}/wizard/state`);
if (!response.ok) throw new Error('API not available');
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, showing wizard');
showWizard();
}
}
function showWizard() {
console.log('showWizard() called, current step:', guidedState.wizardStep);
const wizard = document.getElementById('guided-wizard');
const wasAlreadyActive = wizard.classList.contains('active');
wizard.classList.add('active');
document.getElementById('guided-inbox').classList.remove('active');
// Only reset to step 1 if wizard wasn't already active
if (!wasAlreadyActive) {
console.log('Wizard not active, initializing from step 1');
goToWizardStep(1);
loadTemplatesForWizard();
} else {
console.log('Wizard already active, keeping current step:', guidedState.wizardStep);
}
initWizardEventListeners();
console.log('showWizard() completed');
}
function initWizardEventListeners() {
console.log('initWizardEventListeners() called');
// Use event delegation on the wizard container - more reliable than individual listeners
const wizard = document.getElementById('guided-wizard');
if (!wizard) {
console.log('ERROR: guided-wizard element not found!');
return;
}
console.log('Found wizard element:', wizard);
// Remove any existing handler to prevent duplicates
wizard.removeEventListener('click', handleWizardClick);
wizard.addEventListener('click', handleWizardClick);
console.log('Wizard event delegation initialized successfully');
}
function handleWizardClick(e) {
const target = e.target;
console.log('Wizard click:', target.className, target.id);
// Handle role card clicks (check for card or child elements)
const roleCard = target.closest('.role-card');
if (roleCard && roleCard.dataset.role) {
e.preventDefault();
const role = roleCard.dataset.role;
console.log('Role card clicked:', role);
guidedState.selectedRole = role;
document.querySelectorAll('.role-card').forEach(c => c.classList.remove('selected'));
roleCard.classList.add('selected');
const btn = document.getElementById('wizard-next-1');
if (btn) {
btn.disabled = false;
btn.removeAttribute('disabled');
console.log('Button enabled');
}
return;
}
// Handle Weiter button Step 1
if (target.id === 'wizard-next-1' || target.closest('#wizard-next-1')) {
e.preventDefault();
const btn = document.getElementById('wizard-next-1');
console.log('Next 1 clicked, disabled:', btn ? btn.disabled : 'btn not found');
if (btn && !btn.disabled) {
console.log('Going to step 2');
goToWizardStep(2);
}
return;
}
// Handle Zurueck button Step 2
if (target.id === 'wizard-back-2' || target.closest('#wizard-back-2')) {
e.preventDefault();
console.log('Back to step 1');
goToWizardStep(1);
return;
}
// Handle Weiter button Step 2
if (target.id === 'wizard-next-2' || target.closest('#wizard-next-2')) {
e.preventDefault();
const btn = document.getElementById('wizard-next-2');
console.log('Next 2 clicked, disabled:', btn ? btn.disabled : 'btn not found');
if (btn && !btn.disabled) {
console.log('Going to step 3');
goToWizardStep(3);
}
return;
}
// Handle Zurueck button Step 3
if (target.id === 'wizard-back-3' || target.closest('#wizard-back-3')) {
e.preventDefault();
console.log('Back to step 2');
goToWizardStep(2);
return;
}
// Handle Fertig button Step 3
if (target.id === 'wizard-finish' || target.closest('#wizard-finish')) {
e.preventDefault();
console.log('Finish wizard');
completeWizard();
return;
}
// Handle Skip button
if (target.id === 'wizard-skip-btn' || target.closest('#wizard-skip-btn')) {
e.preventDefault();
if (confirm('Ueberspringen? Sie koennen spaeter anpassen.')) {
switchToExpertMode();
}
return;
}
// Handle template card clicks
const templateCard = target.closest('.template-card');
if (templateCard && templateCard.dataset.templateId) {
e.preventDefault();
const templateId = templateCard.dataset.templateId;
console.log('Template card clicked:', templateId);
toggleTemplate(templateId);
return;
}
}
function showGuidedInbox() {
document.getElementById('guided-wizard').classList.remove('active');
document.getElementById('guided-inbox').classList.add('active');
loadInfoCards();
}
function selectRole(role, element) {
guidedState.selectedRole = role;
document.querySelectorAll('.role-card').forEach(c => c.classList.remove('selected'));
element.classList.add('selected');
const btn = document.getElementById('wizard-next-1');
if (btn) {
btn.disabled = false;
btn.removeAttribute('disabled');
}
}
function goToWizardStep(step) {
console.log('goToWizardStep called with step:', step);
for (let i = 1; i <= 3; i++) {
const stepEl = document.getElementById(`wizard-step-${i}`);
if (stepEl) {
stepEl.classList.remove('active');
console.log(`Step ${i} active removed, display:`, getComputedStyle(stepEl).display);
}
const dotEl = document.getElementById(`wizard-dot-${i}`);
if (dotEl) dotEl.classList.remove('active', 'completed');
if (i < 3) {
const lineEl = document.getElementById(`wizard-line-${i}`);
if (lineEl) lineEl.classList.remove('completed');
}
}
const targetStep = document.getElementById(`wizard-step-${step}`);
if (targetStep) {
targetStep.classList.add('active');
console.log(`Step ${step} activated, display:`, getComputedStyle(targetStep).display);
} else {
console.log('ERROR: wizard-step-' + step + ' not found!');
}
for (let i = 1; i <= step; i++) {
if (i < step) {
const dotEl = document.getElementById(`wizard-dot-${i}`);
if (dotEl) dotEl.classList.add('completed');
if (i < 3) {
const lineEl = document.getElementById(`wizard-line-${i}`);
if (lineEl) lineEl.classList.add('completed');
}
} else {
const dotEl = document.getElementById(`wizard-dot-${i}`);
if (dotEl) dotEl.classList.add('active');
}
}
guidedState.wizardStep = step;
console.log('Wizard step set to:', guidedState.wizardStep);
if (step === 3) updateConfirmation();
}
async function loadTemplatesForWizard() {
try {
const response = await fetch(`${ALERTS_API_BASE}/templates`);
const data = await response.json();
guidedState.templates = data.templates || [];
renderTemplateGrid();
} catch (error) {
guidedState.templates = [
{ id: '1', slug: 'foerderprogramme', name: 'Foerderprogramme', icon: '&#128176;', description: 'Foerdergelder, Antragsfristen' },
{ id: '2', slug: 'abitur-updates', name: 'Abitur-Updates', icon: '&#128221;', description: 'Pruefungstermine, KMK-Beschluesse' },
{ id: '3', slug: 'fortbildungen', name: 'Fortbildungen', icon: '&#127891;', description: 'Seminare, Workshops' },
{ id: '4', slug: 'datenschutz-recht', name: 'Datenschutz & Recht', icon: '&#9878;', description: 'DSGVO-Updates, Urteile' },
{ id: '5', slug: 'it-security', name: 'IT-Security', icon: '&#128274;', description: 'Sicherheitsluecken, Phishing' },
{ id: '6', slug: 'wettbewerbe', name: 'Wettbewerbe', icon: '&#127942;', description: 'Schueler-Wettbewerbe' }
];
renderTemplateGrid();
}
}
function renderTemplateGrid() {
const grid = document.getElementById('template-grid');
grid.innerHTML = guidedState.templates.map(t => `
<div class="template-card ${guidedState.selectedTemplates.includes(t.id) ? 'selected' : ''}"
data-template-id="${t.id}">
<div class="template-card-check">${guidedState.selectedTemplates.includes(t.id) ? '&#10003;' : ''}</div>
<div class="template-card-header">
<div class="template-card-icon">${t.icon}</div>
<div>
<div class="template-card-name">${t.name}</div>
<div class="template-card-desc">${t.description}</div>
</div>
</div>
</div>
`).join('');
}
function toggleTemplate(templateId) {
const idx = guidedState.selectedTemplates.indexOf(templateId);
if (idx > -1) {
guidedState.selectedTemplates.splice(idx, 1);
} else if (guidedState.selectedTemplates.length < 3) {
guidedState.selectedTemplates.push(templateId);
} else {
alert('Maximal 3 Themen auswaehlbar.');
return;
}
renderTemplateGrid();
document.getElementById('template-count').textContent = guidedState.selectedTemplates.length;
const btn2 = document.getElementById('wizard-next-2');
if (guidedState.selectedTemplates.length > 0) {
btn2.disabled = false;
btn2.removeAttribute('disabled');
} else {
btn2.disabled = true;
btn2.setAttribute('disabled', 'disabled');
}
}
function updateConfirmation() {
const roleLabels = { 'lehrkraft': 'Lehrkraft', 'schulleitung': 'Schulleitung', 'it_beauftragte': 'IT-Beauftragte/r' };
document.getElementById('confirm-role').textContent = roleLabels[guidedState.selectedRole] || '-';
document.getElementById('confirm-templates').innerHTML = guidedState.selectedTemplates.map(id => {
const t = guidedState.templates.find(x => x.id === id);
return `<span class="confirmation-template-tag">${t ? t.name : id}</span>`;
}).join('');
}
async function completeWizard() {
const email = document.getElementById('digest-email').value;
try {
await fetch(`${ALERTS_API_BASE}/wizard/step/1`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role: guidedState.selectedRole }) });
await fetch(`${ALERTS_API_BASE}/wizard/step/2`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template_ids: guidedState.selectedTemplates }) });
await fetch(`${ALERTS_API_BASE}/wizard/step/3`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notification_email: email }) });
await fetch(`${ALERTS_API_BASE}/wizard/complete`, { method: 'POST' });
} catch (e) { console.log('Demo mode'); }
guidedState.wizardCompleted = true;
showGuidedInbox();
}
function skipWizard() {
if (confirm('Ueberspringen? Sie koennen spaeter anpassen.')) switchToExpertMode();
}
async function loadInfoCards() {
try {
const response = await fetch(`${ALERTS_API_BASE}/inbox/guided?limit=10`);
if (!response.ok) throw new Error('API not available');
const data = await response.json();
guidedState.infoCards = Array.isArray(data.items) ? data.items : (Array.isArray(data) ? data : []);
renderInfoCards();
} catch (error) {
console.log('Guided inbox API not available, using demo data');
guidedState.infoCards = [
{ id: '1', title: 'DigitalPakt 2.0: Neue Antragsphase', source_name: 'BMBF', importance_level: 'dringend', why_relevant: 'Frist endet in 45 Tagen.', next_steps: ['Schultraeger informieren'], fetched_at: new Date().toISOString() },
{ id: '2', title: 'Kritische Sicherheitsluecke in Moodle', source_name: 'BSI', importance_level: 'kritisch', why_relevant: 'Sofortiges Update erforderlich.', next_steps: ['Systeme pruefen', 'Update einspielen'], fetched_at: new Date().toISOString() }
];
renderInfoCards();
}
}
function renderInfoCards() {
const list = document.getElementById('info-cards-list');
const empty = document.getElementById('guided-empty-state');
document.getElementById('guided-alert-count').textContent = guidedState.infoCards.length;
if (guidedState.infoCards.length === 0) {
list.style.display = 'none';
empty.style.display = 'flex';
return;
}
list.style.display = 'flex';
empty.style.display = 'none';
const importanceLabels = { 'kritisch': 'Kritisch', 'dringend': 'Dringend', 'wichtig': 'Wichtig', 'pruefen': 'Pruefen', 'info': 'Info' };
list.innerHTML = guidedState.infoCards.map(card => {
const lvl = (card.importance_level || 'info').toLowerCase();
const stepsHtml = (card.next_steps || []).map(s => `<div class="info-card-step"><div class="info-card-step-checkbox"></div><span>${escapeHtml(s)}</span></div>`).join('');
return `
<div class="info-card">
<div class="info-card-header">
<span class="importance-badge ${lvl}">${importanceLabels[lvl] || 'Info'}</span>
<span style="font-size: 12px; color: var(--bp-text-muted);">${formatTimeAgo(card.fetched_at)}</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 || '')}</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="window.open('${card.url || '#'}', '_blank')">Oeffnen &#8594;</button>
</div>
</div>
`;
}).join('');
}
async function sendQuickFeedback(cardId, feedbackType) {
try {
await fetch(`${ALERTS_API_BASE}/feedback/quick`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alert_id: cardId, feedback_type: feedbackType }) });
} catch (e) { console.log('Demo mode'); }
guidedState.infoCards = guidedState.infoCards.filter(c => c.id !== cardId);
renderInfoCards();
showToast(feedbackType === 'more_like_this' ? 'Mehr aehnliche Meldungen!' : 'Weniger solche Meldungen.');
}
function showToast(msg) {
const t = document.createElement('div');
t.style.cssText = 'position:fixed;bottom:20px;right:20px;background:#1e293b;color:white;padding:12px 20px;border-radius:8px;font-size:14px;z-index:10000;';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
function showDigestModal() { alert('Wochenbericht wird geladen...'); }
function openGuidedSettings() { guidedState.wizardCompleted = false; showWizard(); }
// Auto-init guided mode when panel becomes visible
(function() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGuidedModeOnLoad);
} else {
setTimeout(initGuidedModeOnLoad, 100);
}
function initGuidedModeOnLoad() {
// Check if guided mode container exists and alerts panel is active
const guidedContainer = document.getElementById('guided-mode-container');
const alertsPanel = document.getElementById('panel-alerts');
// Only initialize if alerts panel is visible
if (guidedContainer && alertsPanel && alertsPanel.style.display !== 'none') {
showWizard();
console.log('Guided Mode initialized');
}
}
// Export init function for module loading
window.loadAlertsModule = function() {
const guidedContainer = document.getElementById('guided-mode-container');
if (guidedContainer) {
showWizard();
console.log('Alerts module loaded');
}
};
})();
"""