(() => { const API_URL = 'https://fb.srv.medeba-media.de/api'; const PROVIDER_MODELS = { gemini: [ { value: '', label: 'Standard (gemini-2.0-flash-exp)' }, { value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash' }, { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }, { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' } ], claude: [ { value: '', label: 'Standard (claude-3-5-haiku)' }, { value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku (schnell)' }, { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet (beste Qualität)' }, { value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' } ], openai: [ { value: '', label: 'Standard (gpt-3.5-turbo)' }, { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (günstig)' }, { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, { value: 'gpt-4o', label: 'GPT-4o' }, { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' } ] }; const PROVIDER_INFO = { gemini: { name: 'Google Gemini', apiKeyLink: 'https://aistudio.google.com/app/apikey', apiKeyHelp: 'API-Schlüssel erforderlich. Erstelle ihn im Google AI Studio.' }, claude: { name: 'Anthropic Claude', apiKeyLink: 'https://console.anthropic.com/settings/keys', apiKeyHelp: 'API-Schlüssel erforderlich. Erstelle ihn in der Anthropic Console.' }, openai: { name: 'OpenAI', apiKeyLink: 'https://platform.openai.com/api-keys', apiKeyHelp: 'Für lokale OpenAI-kompatible Server (z.B. Ollama) kannst du den Schlüssel leer lassen.' } }; let credentials = []; let currentSettings = null; let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 }; const SPORT_WEIGHT_FIELDS = [ { key: 'scoreline', id: 'sportWeightScoreline' }, { key: 'scoreEmoji', id: 'sportWeightScoreEmoji' }, { key: 'sportEmoji', id: 'sportWeightSportEmoji' }, { key: 'sportVerb', id: 'sportWeightSportVerb' }, { key: 'sportNoun', id: 'sportWeightSportNoun' }, { key: 'hashtag', id: 'sportWeightHashtag' }, { key: 'teamToken', id: 'sportWeightTeamToken' }, { key: 'competition', id: 'sportWeightCompetition' }, { key: 'celebration', id: 'sportWeightCelebration' }, { key: 'location', id: 'sportWeightLocation' } ]; const SPORT_TERM_FIELDS = [ { key: 'nouns', id: 'sportTermsNouns', placeholder: 'auswärtssieg, liga, tor ...' }, { key: 'verbs', id: 'sportTermsVerbs', placeholder: 'gewinnen, punkten ...' }, { key: 'competitions', id: 'sportTermsCompetitions', placeholder: 'bundesliga, cup ...' }, { key: 'celebrations', id: 'sportTermsCelebrations', placeholder: 'sieg, tabellenführung ...' }, { key: 'locations', id: 'sportTermsLocations', placeholder: 'auswärts, stadion ...' }, { key: 'negatives', id: 'sportTermsNegatives', placeholder: 'rezept, politik ...' } ]; let moderationSettings = { sports_scoring_enabled: true, sports_score_threshold: 5, sports_score_weights: {}, sports_terms: {}, sports_auto_hide_enabled: false }; function apiFetch(url, options = {}) { return fetch(url, {...options, credentials: 'include'}); } function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 84px; right: 24px; background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'}; color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; z-index: 999999; max-width: 350px; animation: slideIn 0.3s ease-out; pointer-events: none; `; toast.textContent = message; if (!document.getElementById('settings-toast-styles')) { const style = document.createElement('style'); style.id = 'settings-toast-styles'; style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } `; document.head.appendChild(style); } document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease-out'; setTimeout(() => toast.remove(), 300); }, 3000); } function showError(msg) { showToast(msg, 'error'); } function showSuccess(msg) { showToast(msg, 'success'); } async function loadCredentials() { const res = await apiFetch(`${API_URL}/ai-credentials`); if (!res.ok) throw new Error('Failed to load credentials'); credentials = await res.json(); renderCredentials(); updateActiveCredentialSelect(); } async function loadSettings() { const res = await apiFetch(`${API_URL}/ai-settings`); if (!res.ok) throw new Error('Failed to load settings'); currentSettings = await res.json(); document.getElementById('aiEnabled').checked = currentSettings.enabled === 1; document.getElementById('activeCredential').value = currentSettings.active_credential_id || ''; document.getElementById('aiPromptPrefix').value = currentSettings.prompt_prefix || 'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n'; } async function loadHiddenSettings() { const res = await apiFetch(`${API_URL}/hidden-settings`); if (!res.ok) throw new Error('Failed to load hidden settings'); hiddenSettings = await res.json(); applyHiddenSettingsUI(); } function applyHiddenSettingsUI() { const autoToggle = document.getElementById('autoPurgeHiddenToggle'); const retentionInput = document.getElementById('hiddenRetentionDays'); if (autoToggle) { autoToggle.checked = !!hiddenSettings.auto_purge_enabled; } if (retentionInput) { retentionInput.value = hiddenSettings.retention_days || 90; retentionInput.disabled = !autoToggle?.checked; } } function normalizeRetentionInput(value) { const parsed = parseInt(value, 10); if (Number.isNaN(parsed) || parsed <= 0) { return 90; } return Math.min(365, Math.max(1, parsed)); } async function saveHiddenSettings(event, { silent = false } = {}) { if (event && typeof event.preventDefault === 'function') { event.preventDefault(); } const autoToggle = document.getElementById('autoPurgeHiddenToggle'); const retentionInput = document.getElementById('hiddenRetentionDays'); const autoEnabled = autoToggle ? autoToggle.checked : true; const retention = normalizeRetentionInput(retentionInput ? retentionInput.value : hiddenSettings.retention_days); try { const res = await apiFetch(`${API_URL}/hidden-settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auto_purge_enabled: autoEnabled, retention_days: retention }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Speichern'); } hiddenSettings = await res.json(); applyHiddenSettingsUI(); if (!silent) { showSuccess('✅ Einstellungen für versteckte Beiträge gespeichert'); } return true; } catch (err) { if (!silent) { showError('❌ ' + err.message); } return false; } } async function purgeHiddenNow() { const btn = document.getElementById('purgeHiddenNowBtn'); const originalText = btn ? btn.textContent : ''; if (btn) { btn.disabled = true; btn.textContent = 'Bereinige...'; } try { const res = await apiFetch(`${API_URL}/search-posts`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Bereinigen'); } showSuccess('🧹 Versteckte Beiträge wurden zurückgesetzt'); } catch (err) { showError('❌ ' + err.message); } finally { if (btn) { btn.disabled = false; btn.textContent = originalText || 'Jetzt bereinigen'; } } } function normalizeSportsScoreThresholdInput(value) { const parsed = parseFloat(value); if (Number.isNaN(parsed) || parsed < 0) { return 5; } return Math.min(50, Math.max(0, parsed)); } function normalizeSportWeightInput(value, fallback = 1) { const parsed = parseFloat(value); if (Number.isNaN(parsed) || parsed < 0) { return fallback; } return Math.min(10, Math.max(0, parsed)); } function applyWeightInputs(enabled) { SPORT_WEIGHT_FIELDS.forEach(({ id }) => { const input = document.getElementById(id); if (input) { input.disabled = !enabled; } }); } function applyTermInputs(enabled) { SPORT_TERM_FIELDS.forEach(({ id }) => { const input = document.getElementById(id); if (input) { input.disabled = !enabled; } }); } function serializeTermListInput(value) { if (!value || typeof value !== 'string') return []; return value .split(',') .map((entry) => entry.trim()) .filter((entry) => entry) .slice(0, 200); } function renderTermList(list) { if (!Array.isArray(list) || !list.length) return ''; return list.join(', '); } function applyModerationSettingsUI() { const enabledToggle = document.getElementById('sportsScoringEnabled'); const thresholdInput = document.getElementById('sportsScoreThreshold'); const autoHideToggle = document.getElementById('sportsAutoHideEnabled'); if (enabledToggle) { enabledToggle.checked = !!moderationSettings.sports_scoring_enabled; } if (thresholdInput) { thresholdInput.value = moderationSettings.sports_score_threshold ?? 5; thresholdInput.disabled = !enabledToggle?.checked; } if (autoHideToggle) { autoHideToggle.checked = !!moderationSettings.sports_auto_hide_enabled; autoHideToggle.disabled = !enabledToggle?.checked; } SPORT_WEIGHT_FIELDS.forEach(({ key, id }) => { const input = document.getElementById(id); if (input) { const value = moderationSettings.sports_score_weights?.[key]; input.value = typeof value === 'number' ? value : ''; input.disabled = !enabledToggle?.checked; } }); SPORT_TERM_FIELDS.forEach(({ key, id }) => { const input = document.getElementById(id); if (input) { input.value = renderTermList(moderationSettings.sports_terms?.[key]); input.disabled = !enabledToggle?.checked; } }); } async function loadModerationSettings() { const res = await apiFetch(`${API_URL}/moderation-settings`); if (!res.ok) throw new Error('Konnte Moderations-Einstellungen nicht laden'); const data = await res.json(); moderationSettings = { sports_scoring_enabled: !!data.sports_scoring_enabled, sports_score_threshold: normalizeSportsScoreThresholdInput(data.sports_score_threshold), sports_score_weights: data.sports_score_weights || {}, sports_terms: data.sports_terms || {}, sports_auto_hide_enabled: !!data.sports_auto_hide_enabled }; applyModerationSettingsUI(); } async function saveModerationSettings(event, { silent = false } = {}) { if (event && typeof event.preventDefault === 'function') { event.preventDefault(); } const enabledToggle = document.getElementById('sportsScoringEnabled'); const thresholdInput = document.getElementById('sportsScoreThreshold'); const autoHideToggle = document.getElementById('sportsAutoHideEnabled'); const enabled = enabledToggle ? enabledToggle.checked : true; const threshold = thresholdInput ? normalizeSportsScoreThresholdInput(thresholdInput.value) : moderationSettings.sports_score_threshold; const autoHide = autoHideToggle ? autoHideToggle.checked : false; const weights = {}; SPORT_WEIGHT_FIELDS.forEach(({ key, id }) => { const input = document.getElementById(id); weights[key] = normalizeSportWeightInput(input ? input.value : moderationSettings.sports_score_weights?.[key], moderationSettings.sports_score_weights?.[key] ?? 1); }); const terms = {}; SPORT_TERM_FIELDS.forEach(({ key, id }) => { const input = document.getElementById(id); terms[key] = serializeTermListInput(input ? input.value : moderationSettings.sports_terms?.[key]); }); try { const res = await apiFetch(`${API_URL}/moderation-settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sports_scoring_enabled: enabled, sports_score_threshold: threshold, sports_auto_hide_enabled: autoHide, sports_score_weights: weights, sports_terms: terms }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Speichern'); } moderationSettings = await res.json(); applyModerationSettingsUI(); if (!silent) { showSuccess('✅ Sport-Scoring gespeichert'); } return true; } catch (err) { if (!silent) { showError('❌ ' + err.message); } return false; } } function shorten(text, maxLength = 80) { if (typeof text !== 'string') { return ''; } if (text.length <= maxLength) { return text; } return `${text.slice(0, maxLength - 3)}...`; } function escapeHtmlAttr(text) { return escapeHtml(text || '').replace(/"/g, '"'); } function formatTimeLabel(iso) { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } function formatRelativePast(iso) { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; const diffMs = Date.now() - date.getTime(); if (diffMs < 0) return 'gerade eben'; const diffMinutes = Math.round(diffMs / 60000); if (diffMinutes <= 1) return 'gerade eben'; if (diffMinutes < 60) return `vor ${diffMinutes} Min`; const diffHours = Math.round(diffMinutes / 60); if (diffHours < 24) return `vor ${diffHours} Std`; const diffDays = Math.round(diffHours / 24); if (diffDays === 1) return 'gestern'; if (diffDays < 7) return `vor ${diffDays} Tagen`; return date.toLocaleDateString('de-DE'); } function formatRelativeFuture(iso) { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; const diffMs = date.getTime() - Date.now(); if (diffMs <= 0) return 'gleich'; const diffMinutes = Math.round(diffMs / 60000); if (diffMinutes < 1) return 'gleich'; if (diffMinutes < 60) return `in ${diffMinutes} Min`; const diffHours = Math.round(diffMinutes / 60); if (diffHours < 24) return `in ${diffHours} Std`; return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } function buildCredentialBadges(credential) { const badges = []; if (!credential.is_active) { badges.push({ label: 'Deaktiviert', className: 'status-badge--muted', title: 'Dieser Login ist derzeit deaktiviert' }); } else if (credential.auto_disabled) { const untilText = credential.auto_disabled_until ? formatRelativeFuture(credential.auto_disabled_until) : 'läuft'; const untilTime = credential.auto_disabled_until ? formatTimeLabel(credential.auto_disabled_until) : ''; const reason = credential.auto_disabled_reason ? credential.auto_disabled_reason.replace(/^AUTO:/, '').trim() : 'Automatisch deaktiviert'; badges.push({ label: `Cooldown ${untilText}${untilTime ? ` (${untilTime})` : ''}`.trim(), className: 'status-badge--warning', title: reason || 'Automatisch deaktiviert' }); } else { badges.push({ label: 'Aktiv', className: 'status-badge--success', title: 'Login ist aktiv' }); } if (credential.last_error_message) { badges.push({ label: `Fehler ${formatRelativePast(credential.last_error_at)}`.trim(), className: 'status-badge--danger', title: credential.last_error_message }); } if (credential.usage_24h_count) { const resetHint = credential.usage_24h_reset_at ? `Reset ${formatRelativeFuture(credential.usage_24h_reset_at)}` : '24h Nutzung'; badges.push({ label: `24h: ${credential.usage_24h_count}`, className: 'status-badge--info', title: resetHint }); } if (credential.last_rate_limit_remaining) { badges.push({ label: `Limit: ${credential.last_rate_limit_remaining}`, className: 'status-badge--neutral', title: 'Letzter „rate limit remaining“-Wert' }); } return badges; } function buildCredentialMetaLines(credential) { const lines = []; if (credential.last_success_at) { lines.push(`Zuletzt erfolgreich: ${formatRelativePast(credential.last_success_at)}`); } if (!credential.last_success_at && credential.last_used_at) { lines.push(`Zuletzt genutzt: ${formatRelativePast(credential.last_used_at)}`); } if (credential.rate_limit_reset_at && !credential.auto_disabled) { lines.push(`Limit-Reset ${formatRelativeFuture(credential.rate_limit_reset_at)}`); } if (credential.latest_event && credential.latest_event.type) { const typeLabel = credential.latest_event.type.replace(/_/g, ' '); const eventTime = credential.latest_event.created_at ? formatRelativePast(credential.latest_event.created_at) : ''; const message = credential.latest_event.message ? shorten(credential.latest_event.message, 90) : ''; const parts = [`Letztes Event (${typeLabel})`]; if (eventTime) parts.push(eventTime); if (message) parts.push(`– ${message}`); lines.push(parts.join(' ')); } return lines; } function renderCredentials() { const list = document.getElementById('credentialsList'); if (!credentials.length) { list.innerHTML = '
Noch keine Anmeldedaten gespeichert
'; return; } list.innerHTML = credentials.map((c, index) => { const providerName = escapeHtml(PROVIDER_INFO[c.provider]?.name || c.provider); const modelLabel = c.model ? ` · ${escapeHtml(c.model)}` : ''; const endpointLabel = c.base_url ? ` · ${escapeHtml(c.base_url)}` : ''; const badges = buildCredentialBadges(c); const badgesHtml = badges.length ? `Kommagetrennte Liste von Freundesnamen für Profil ${i}
`; list.appendChild(div); document.getElementById(`friends${i}`).addEventListener('blur', async (e) => { const newValue = e.target.value.trim(); if (newValue !== profileFriends[i]) { await saveFriends(i, newValue); } }); } } async function saveFriends(profileNumber, friendNames, { silent = false } = {}) { try { const res = await apiFetch(`${API_URL}/profile-friends/${profileNumber}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ friend_names: friendNames }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Fehler beim Speichern'); } profileFriends[profileNumber] = friendNames; if (!silent) { showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`); } return true; } catch (err) { if (!silent) { showError('❌ ' + err.message); } return false; } } async function saveAllFriends({ silent = false } = {}) { let success = true; for (let i = 1; i <= 5; i++) { const input = document.getElementById(`friends${i}`); if (!input) { continue; } const newValue = input.value.trim(); if (newValue !== profileFriends[i]) { const result = await saveFriends(i, newValue, { silent }); success = success && result; } } return success; } // Event listeners document.getElementById('addCredentialBtn').addEventListener('click', () => openCredentialModal()); document.getElementById('credentialModalClose').addEventListener('click', closeCredentialModal); document.getElementById('credentialCancelBtn').addEventListener('click', closeCredentialModal); document.getElementById('credentialForm').addEventListener('submit', saveCredential); document.getElementById('credentialProvider').addEventListener('change', e => updateModelOptions(e.target.value)); document.getElementById('aiSettingsForm').addEventListener('submit', (e) => { e.preventDefault(); saveSettings(e); }); document.getElementById('testBtn').addEventListener('click', testComment); document.getElementById('testModalClose').addEventListener('click', () => document.getElementById('testModal').setAttribute('hidden', '')); document.getElementById('generateTestComment').addEventListener('click', generateTest); document.getElementById('purgeHiddenNowBtn').addEventListener('click', purgeHiddenNow); document.getElementById('saveAllFloatingBtn').addEventListener('click', saveAllSettings); const autoPurgeHiddenToggle = document.getElementById('autoPurgeHiddenToggle'); if (autoPurgeHiddenToggle) { autoPurgeHiddenToggle.addEventListener('change', () => { const retentionInput = document.getElementById('hiddenRetentionDays'); if (retentionInput) { retentionInput.disabled = !autoPurgeHiddenToggle.checked; } }); } const sportsScoringToggle = document.getElementById('sportsScoringEnabled'); const sportsScoreInput = document.getElementById('sportsScoreThreshold'); if (sportsScoringToggle && sportsScoreInput) { sportsScoringToggle.addEventListener('change', () => { sportsScoreInput.disabled = !sportsScoringToggle.checked; applyWeightInputs(sportsScoringToggle.checked); applyTermInputs(sportsScoringToggle.checked); const autoHideToggle = document.getElementById('sportsAutoHideEnabled'); if (autoHideToggle) { autoHideToggle.disabled = !sportsScoringToggle.checked; } }); sportsScoreInput.addEventListener('blur', () => { sportsScoreInput.value = normalizeSportsScoreThresholdInput(sportsScoreInput.value); }); SPORT_WEIGHT_FIELDS.forEach(({ id }) => { const input = document.getElementById(id); if (input) { input.addEventListener('blur', () => { input.value = normalizeSportWeightInput(input.value); }); } }); SPORT_TERM_FIELDS.forEach(({ id }) => { const input = document.getElementById(id); if (input) { input.addEventListener('blur', () => { input.value = renderTermList(serializeTermListInput(input.value)); }); } }); const moderationForm = document.getElementById('moderationSettingsForm'); if (moderationForm) { moderationForm.addEventListener('submit', (e) => saveModerationSettings(e)); } } // Initialize Promise.all([ loadCredentials(), loadSettings(), loadHiddenSettings(), loadModerationSettings(), loadProfileFriends() ]).catch(err => showError(err.message)); })();