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>
1108 lines
37 KiB
Python
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">✓</button>
|
|
<button class="alert-action-btn drop" onclick="event.stopPropagation(); markAlertDrop('${alert.id}')" title="Verwerfen">✗</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">📰</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}')">
|
|
↻ Abrufen
|
|
</button>
|
|
<button class="topic-card-btn topic-card-btn-secondary" onclick="editTopic('${topic.id}')">
|
|
✎ Bearbeiten
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('') + `
|
|
<div class="topic-card topic-card-add" onclick="openAddTopicModal()">
|
|
<div class="topic-card-add-icon">➕</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">📋</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">☰</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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// 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: '💰', description: 'Foerdergelder, Antragsfristen' },
|
|
{ id: '2', slug: 'abitur-updates', name: 'Abitur-Updates', icon: '📝', description: 'Pruefungstermine, KMK-Beschluesse' },
|
|
{ id: '3', slug: 'fortbildungen', name: 'Fortbildungen', icon: '🎓', description: 'Seminare, Workshops' },
|
|
{ id: '4', slug: 'datenschutz-recht', name: 'Datenschutz & Recht', icon: '⚖', description: 'DSGVO-Updates, Urteile' },
|
|
{ id: '5', slug: 'it-security', name: 'IT-Security', icon: '🔒', description: 'Sicherheitsluecken, Phishing' },
|
|
{ id: '6', slug: 'wettbewerbe', name: 'Wettbewerbe', icon: '🏆', 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) ? '✓' : ''}</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">💡 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')">👎 Nicht relevant</button>
|
|
<button class="feedback-btn positive" onclick="sendQuickFeedback('${card.id}', 'more_like_this')">👍 Mehr davon</button>
|
|
</div>
|
|
<button class="info-card-open" onclick="window.open('${card.url || '#'}', '_blank')">Oeffnen →</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');
|
|
}
|
|
};
|
|
})();
|
|
"""
|