Files
PostTracker/web/settings.js
2025-11-24 16:32:13 +01:00

1202 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
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, '&quot;');
}
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 = '<p class="empty-state">Noch keine Anmeldedaten gespeichert</p>';
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
? `<div class="credential-item__status">${badges.map(badge => `<span class="credential-status ${badge.className}"${badge.title ? ` title="${escapeHtmlAttr(badge.title)}"` : ''}>${escapeHtml(badge.label)}</span>`).join('')}</div>`
: '';
const metaLines = buildCredentialMetaLines(c);
const metaHtml = metaLines.length
? `<div class="credential-item__meta">${metaLines.map(line => `<div>${escapeHtml(line)}</div>`).join('')}</div>`
: '';
return `
<div class="credential-item" draggable="true" data-credential-id="${c.id}" data-index="${index}">
<div class="credential-item__drag-handle" title="Ziehen zum Sortieren">⋮⋮</div>
<div class="credential-item__info">
<label class="form-label" style="display: flex; align-items: center; gap: 8px; margin: 0;">
<input type="checkbox" class="form-checkbox"
${c.is_active ? 'checked' : ''}
onchange="toggleCredentialActive(${c.id}, this.checked)">
<div>
<div class="credential-item__name">${escapeHtml(c.name)}</div>
<div class="credential-item__provider">${providerName}${modelLabel}${endpointLabel}</div>
${badgesHtml}
${metaHtml}
</div>
</label>
</div>
<div class="credential-item__actions">
<button onclick="editCredential(${c.id})" class="btn-icon" title="Bearbeiten">✏️</button>
<button onclick="deleteCredential(${c.id})" class="btn-icon" title="Löschen">🗑️</button>
</div>
</div>
`;
}).join('');
// Add drag and drop event listeners
setupDragAndDrop();
}
function updateActiveCredentialSelect() {
const select = document.getElementById('activeCredential');
select.innerHTML = '<option value="">-- Bitte wählen --</option>' +
credentials.map(c => `<option value="${c.id}">${escapeHtml(c.name)} (${PROVIDER_INFO[c.provider]?.name})</option>`).join('');
if (currentSettings?.active_credential_id) {
select.value = currentSettings.active_credential_id;
}
}
function updateModelOptions(provider) {
const modelInput = document.getElementById('credentialModel');
const modelList = document.getElementById('credentialModelOptions');
const apiKeyInput = document.getElementById('credentialApiKey');
const baseUrlGroup = document.getElementById('credentialBaseUrlGroup');
const baseUrlHelp = document.getElementById('credentialBaseUrlHelp');
const baseUrlInput = document.getElementById('credentialBaseUrl');
const info = PROVIDER_INFO[provider];
const models = PROVIDER_MODELS[provider] || [];
if (modelList) {
modelList.innerHTML = models.map(m => `<option value="${m.value}">${m.label}</option>`).join('');
}
if (modelInput) {
const firstSuggestion = models.find(m => m.value)?.value;
modelInput.placeholder = firstSuggestion
? `z.B. ${firstSuggestion}`
: 'Modell-ID (z.B. llama3.1)';
}
const help = document.getElementById('credentialApiKeyHelp');
if (help) {
if (info) {
const parts = [];
if (info.apiKeyHelp) {
parts.push(info.apiKeyHelp);
}
if (info.apiKeyLink) {
parts.push(`<a href="${info.apiKeyLink}" target="_blank">API-Schlüssel erstellen</a>`);
}
help.innerHTML = parts.join(' ');
} else {
help.textContent = '';
}
}
if (apiKeyInput) {
if (provider === 'openai') {
apiKeyInput.placeholder = 'sk-... oder leer für lokale Server';
} else {
apiKeyInput.placeholder = 'API-Schlüssel';
}
}
if (baseUrlGroup && baseUrlHelp) {
if (provider === 'openai') {
baseUrlGroup.style.display = 'block';
baseUrlHelp.textContent = 'Leer lassen für die offizielle OpenAI-API. Für lokale OpenAI/Ollama-Server gib die Basis-URL an, z.B. http://localhost:11434/v1';
if (baseUrlInput) {
baseUrlInput.placeholder = 'https://api.openai.com/v1 oder http://localhost:11434/v1';
}
} else {
baseUrlGroup.style.display = 'none';
baseUrlHelp.textContent = '';
if (baseUrlInput) {
baseUrlInput.placeholder = '';
}
}
}
const modelHelp = document.getElementById('credentialModelHelp');
if (modelHelp) {
modelHelp.textContent = 'Trage die Modell-ID ein. Du kannst einen Vorschlag auswählen oder einen eigenen Wert eingeben.';
}
}
function openCredentialModal(credential = null) {
const modal = document.getElementById('credentialModal');
const form = document.getElementById('credentialForm');
const apiKeyInput = document.getElementById('credentialApiKey');
const baseUrlInput = document.getElementById('credentialBaseUrl');
if (credential) {
document.getElementById('credentialModalTitle').textContent = 'Anmeldedaten bearbeiten';
document.getElementById('credentialId').value = credential.id;
document.getElementById('credentialName').value = credential.name;
document.getElementById('credentialProvider').value = credential.provider;
updateModelOptions(credential.provider);
document.getElementById('credentialModel').value = credential.model || '';
if (baseUrlInput) {
baseUrlInput.value = credential.base_url || '';
}
if (apiKeyInput) {
apiKeyInput.value = '';
apiKeyInput.placeholder = credential.provider === 'openai'
? 'Leer lassen, um den bestehenden Schlüssel zu behalten'
: 'Leer lassen, um den bestehenden Schlüssel zu behalten';
}
} else {
document.getElementById('credentialModalTitle').textContent = 'Anmeldedaten hinzufügen';
form.reset();
updateModelOptions('gemini');
document.getElementById('credentialId').value = '';
if (apiKeyInput) {
apiKeyInput.value = '';
apiKeyInput.placeholder = 'API-Schlüssel';
}
if (baseUrlInput) {
baseUrlInput.value = '';
}
}
modal.removeAttribute('hidden');
}
function closeCredentialModal() {
document.getElementById('credentialModal').setAttribute('hidden', '');
}
async function saveCredential(e) {
e.preventDefault();
try {
const id = document.getElementById('credentialId').value;
const name = document.getElementById('credentialName').value.trim();
const provider = document.getElementById('credentialProvider').value;
const apiKey = document.getElementById('credentialApiKey').value.trim();
const model = document.getElementById('credentialModel').value.trim();
const baseUrlRaw = document.getElementById('credentialBaseUrl')?.value.trim() || '';
if (!name) {
throw new Error('Bitte einen Namen angeben');
}
const data = {
name,
provider,
model: model || null,
base_url: provider === 'openai' ? baseUrlRaw : ''
};
if (!id) {
if (!apiKey && !(provider === 'openai' && baseUrlRaw)) {
throw new Error('API-Schlüssel ist erforderlich (oder Basis-URL für lokale OpenAI-kompatible Server angeben)');
}
data.api_key = apiKey;
} else if (apiKey) {
data.api_key = apiKey;
}
const url = id ? `${API_URL}/ai-credentials/${id}` : `${API_URL}/ai-credentials`;
const method = id ? 'PUT' : 'POST';
const res = await apiFetch(url, {
method,
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Fehler beim Speichern der Anmeldedaten');
}
await loadCredentials();
closeCredentialModal();
showSuccess('✅ Anmeldedaten erfolgreich gespeichert');
} catch (err) {
showError('❌ ' + err.message);
}
}
async function editCredential(id) {
const cred = credentials.find(c => c.id === id);
if (!cred) {
showError('Anmeldedaten nicht gefunden');
return;
}
openCredentialModal(cred);
}
async function toggleCredentialActive(id, isActive) {
try {
const res = await apiFetch(`${API_URL}/ai-credentials/${id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ is_active: isActive ? 1 : 0 })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Fehler beim Aktualisieren');
}
await loadCredentials();
showSuccess(`✅ Login ${isActive ? 'aktiviert' : 'deaktiviert'}`);
} catch (err) {
showError('❌ ' + err.message);
await loadCredentials(); // Reload to reset checkbox
}
}
async function deleteCredential(id) {
if (!confirm('Wirklich löschen?')) return;
const res = await apiFetch(`${API_URL}/ai-credentials/${id}`, {method: 'DELETE'});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Failed to delete');
}
await loadCredentials();
showSuccess('Anmeldedaten gelöscht');
}
// Make functions globally accessible for inline handlers
window.toggleCredentialActive = toggleCredentialActive;
window.editCredential = editCredential;
window.deleteCredential = deleteCredential;
// ============================================================================
// DRAG AND DROP
// ============================================================================
let draggedElement = null;
function setupDragAndDrop() {
const items = document.querySelectorAll('.credential-item');
items.forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('dragenter', handleDragEnter);
item.addEventListener('dragleave', handleDragLeave);
});
}
function handleDragStart(e) {
draggedElement = this;
this.style.opacity = '0.4';
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDragEnter(e) {
if (this !== draggedElement) {
this.classList.add('drag-over');
}
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedElement !== this) {
// Get the container
const container = this.parentNode;
const allItems = [...container.querySelectorAll('.credential-item')];
// Get indices
const draggedIndex = allItems.indexOf(draggedElement);
const targetIndex = allItems.indexOf(this);
// Reorder in DOM
if (draggedIndex < targetIndex) {
this.parentNode.insertBefore(draggedElement, this.nextSibling);
} else {
this.parentNode.insertBefore(draggedElement, this);
}
// Update backend
saveCredentialOrder();
}
this.classList.remove('drag-over');
return false;
}
function handleDragEnd(e) {
this.style.opacity = '1';
// Remove all drag-over classes
document.querySelectorAll('.credential-item').forEach(item => {
item.classList.remove('drag-over');
});
}
async function saveCredentialOrder() {
try {
const items = document.querySelectorAll('.credential-item');
const order = Array.from(items).map(item => parseInt(item.dataset.credentialId));
const res = await apiFetch(`${API_URL}/ai-credentials/reorder`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ order })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Fehler beim Speichern der Reihenfolge');
}
credentials = await res.json();
showSuccess('✅ Reihenfolge gespeichert');
} catch (err) {
showError('❌ ' + err.message);
await loadCredentials(); // Reload to restore original order
}
}
async function saveSettings(e, { silent = false } = {}) {
if (e && typeof e.preventDefault === 'function') {
e.preventDefault();
}
try {
const data = {
enabled: document.getElementById('aiEnabled').checked,
active_credential_id: parseInt(document.getElementById('activeCredential').value) || null,
prompt_prefix: document.getElementById('aiPromptPrefix').value
};
const res = await apiFetch(`${API_URL}/ai-settings`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Fehler beim Speichern der Einstellungen');
}
currentSettings = await res.json();
if (!silent) {
showSuccess('✅ Einstellungen erfolgreich gespeichert');
}
return true;
} catch (err) {
if (!silent) {
showError('❌ ' + err.message);
}
return false;
}
}
async function testComment() {
const modal = document.getElementById('testModal');
modal.removeAttribute('hidden');
document.getElementById('testResult').style.display = 'none';
document.getElementById('testError').style.display = 'none';
// Load last test data from localStorage
const lastTest = localStorage.getItem('lastTestComment');
if (lastTest) {
try {
const data = JSON.parse(lastTest);
document.getElementById('testPostText').value = data.postText || '';
document.getElementById('testProfileNumber').value = data.profileNumber || '1';
} catch (e) {
console.error('Failed to load last test comment:', e);
}
}
}
async function generateTest() {
const text = document.getElementById('testPostText').value;
const profileNumber = parseInt(document.getElementById('testProfileNumber').value);
if (!text) return;
document.getElementById('testLoading').style.display = 'block';
document.getElementById('testResult').style.display = 'none';
document.getElementById('testError').style.display = 'none';
try {
const res = await apiFetch(`${API_URL}/ai/generate-comment`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({postText: text, profileNumber})
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Failed');
}
const data = await res.json();
document.getElementById('testComment').textContent = data.comment;
document.getElementById('testResult').style.display = 'block';
// Save test data to localStorage
localStorage.setItem('lastTestComment', JSON.stringify({
postText: text,
profileNumber: profileNumber,
comment: data.comment,
timestamp: new Date().toISOString()
}));
} catch (err) {
document.getElementById('testError').textContent = err.message;
document.getElementById('testError').style.display = 'block';
} finally {
document.getElementById('testLoading').style.display = 'none';
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function saveAllSettings(event) {
if (event && typeof event.preventDefault === 'function') {
event.preventDefault();
}
const saveBtn = document.getElementById('saveAllFloatingBtn');
const labelEl = saveBtn ? saveBtn.querySelector('.label') : null;
const spinnerEl = saveBtn ? saveBtn.querySelector('.spinner') : null;
const defaultLabel = saveBtn
? (saveBtn.dataset.defaultLabel || (labelEl && labelEl.textContent.trim()) || 'Einstellungen speichern')
: 'Einstellungen speichern';
if (saveBtn) {
saveBtn.dataset.defaultLabel = defaultLabel;
}
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.classList.add('is-saving');
if (labelEl) {
labelEl.textContent = 'Speichern...';
} else {
saveBtn.textContent = 'Speichern...';
}
if (spinnerEl) {
spinnerEl.style.display = 'inline-block';
spinnerEl.classList.add('spin');
}
}
const results = await Promise.all([
saveSettings(null, { silent: true }),
saveHiddenSettings(null, { silent: true }),
saveModerationSettings(null, { silent: true }),
saveAllFriends({ silent: true })
]);
const allOk = results.every(Boolean);
if (allOk) {
showSuccess('Gespeichert');
} else {
showError('❌ Nicht alle Einstellungen konnten gespeichert werden');
}
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.classList.remove('is-saving');
const label = saveBtn.querySelector('.label');
const spinner = saveBtn.querySelector('.spinner');
if (label) {
label.textContent = saveBtn.dataset.defaultLabel || 'Einstellungen speichern';
} else {
saveBtn.textContent = 'Einstellungen speichern';
}
if (spinner) {
spinner.style.display = 'none';
spinner.classList.remove('spin');
}
}
}
// ============================================================================
// PROFILE FRIENDS
// ============================================================================
let profileFriends = {};
async function loadProfileFriends() {
const list = document.getElementById('profileFriendsList');
list.innerHTML = '';
for (let i = 1; i <= 5; i++) {
const res = await apiFetch(`${API_URL}/profile-friends/${i}`);
const data = await res.json();
profileFriends[i] = data.friend_names || '';
const div = document.createElement('div');
div.className = 'form-group';
div.innerHTML = `
<label for="friends${i}" class="form-label">Profil ${i}</label>
<input type="text" id="friends${i}" class="form-input"
placeholder="z.B. Anna, Max, Lisa"
value="${escapeHtml(profileFriends[i])}">
<p class="form-help">Kommagetrennte Liste von Freundesnamen für Profil ${i}</p>
`;
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));
})();