From 2e4a6ae7c4e4a87d7258ffe0d4ca0856a3c7236a Mon Sep 17 00:00:00 2001 From: Meik Date: Fri, 21 Nov 2025 14:33:34 +0100 Subject: [PATCH] reworked settings page --- backend/server.js | 91 +++++++++++++++++++- web/index.html | 63 ++++++++++---- web/settings.css | 67 +++++++++++++++ web/settings.js | 211 +++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 403 insertions(+), 29 deletions(-) diff --git a/backend/server.js b/backend/server.js index f11ca25..8e3763d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -794,6 +794,15 @@ db.exec(` ); `); +db.exec(` + CREATE TABLE IF NOT EXISTS maintenance_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + search_retention_days INTEGER DEFAULT ${SEARCH_POST_RETENTION_DAYS}, + auto_purge_hidden INTEGER DEFAULT 1, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); +`); + db.exec(` CREATE INDEX IF NOT EXISTS idx_search_seen_posts_last_seen_at ON search_seen_posts(last_seen_at); @@ -1546,9 +1555,62 @@ function getFormattedCredentialById(id) { return formatCredentialRow(row[0]); } +function normalizeRetentionDays(value) { + const parsed = parseInt(value, 10); + if (Number.isNaN(parsed) || parsed <= 0) { + return SEARCH_POST_RETENTION_DAYS; + } + return Math.min(365, Math.max(1, parsed)); +} + +function loadHiddenSettings() { + let settings = db.prepare('SELECT * FROM maintenance_settings WHERE id = 1').get(); + if (!settings) { + const defaults = { + id: 1, + search_retention_days: SEARCH_POST_RETENTION_DAYS, + auto_purge_hidden: 1 + }; + db.prepare(` + INSERT INTO maintenance_settings (id, search_retention_days, auto_purge_hidden, updated_at) + VALUES (1, ?, ?, CURRENT_TIMESTAMP) + `).run(defaults.search_retention_days, defaults.auto_purge_hidden); + settings = defaults; + } + settings.search_retention_days = normalizeRetentionDays(settings.search_retention_days); + settings.auto_purge_hidden = settings.auto_purge_hidden ? 1 : 0; + return settings; +} + +function persistHiddenSettings({ retentionDays, autoPurgeEnabled }) { + const normalizedRetention = normalizeRetentionDays(retentionDays); + const normalizedAuto = autoPurgeEnabled ? 1 : 0; + const existing = db.prepare('SELECT id FROM maintenance_settings WHERE id = 1').get(); + if (existing) { + db.prepare(` + UPDATE maintenance_settings + SET search_retention_days = ?, auto_purge_hidden = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = 1 + `).run(normalizedRetention, normalizedAuto); + } else { + db.prepare(` + INSERT INTO maintenance_settings (id, search_retention_days, auto_purge_hidden, updated_at) + VALUES (1, ?, ?, CURRENT_TIMESTAMP) + `).run(normalizedRetention, normalizedAuto); + } + return { + auto_purge_enabled: !!normalizedAuto, + retention_days: normalizedRetention + }; +} + function cleanupExpiredSearchPosts() { try { - const threshold = `-${SEARCH_POST_RETENTION_DAYS} day`; + const settings = loadHiddenSettings(); + if (!settings.auto_purge_hidden) { + return; + } + const threshold = `-${settings.search_retention_days} day`; db.prepare(` DELETE FROM search_seen_posts WHERE last_seen_at < DATETIME('now', ?) @@ -3197,6 +3259,33 @@ app.put('/api/ai-settings', (req, res) => { } }); +app.get('/api/hidden-settings', (req, res) => { + try { + const settings = loadHiddenSettings(); + res.json({ + auto_purge_enabled: !!settings.auto_purge_hidden, + retention_days: settings.search_retention_days + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.put('/api/hidden-settings', (req, res) => { + try { + const body = req.body || {}; + const retentionDays = normalizeRetentionDays(body.retention_days); + const autoPurgeEnabled = !!body.auto_purge_enabled; + const saved = persistHiddenSettings({ retentionDays, autoPurgeEnabled }); + if (saved.auto_purge_enabled) { + cleanupExpiredSearchPosts(); + } + res.json(saved); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + function sanitizeAIComment(text) { if (!text) { return ''; diff --git a/web/index.html b/web/index.html index bc538d5..7c6ced0 100644 --- a/web/index.html +++ b/web/index.html @@ -428,6 +428,16 @@ + +
+

👥 Freundesnamen pro Profil

+

+ Gib für jedes Profil eine Liste von Freundesnamen an, die im Prompt verwendet werden können. +

+ +
+
+

AI-Kommentar-Generator

@@ -461,32 +471,55 @@ Dieser Text wird vor dem eigentlichen Post-Text an die KI gesendet. Verwende {FREUNDE} als Platzhalter für Freundesnamen.

+ +
+ +
- +
-

👥 Freundesnamen pro Profil

+

Versteckte Beiträge bereinigen

- Gib für jedes Profil eine Liste von Freundesnamen an, die im Prompt verwendet werden können. + Steuere die automatische Bereinigung versteckter/ausgeblendeter Beiträge aus der Suche und starte bei Bedarf eine manuelle Bereinigung.

-
-
+
+
+ +

+ Löscht versteckte Beiträge nach Ablauf der Aufbewahrungsdauer automatisch. +

+
- -
-
- - -
+
+ + +

+ Ältere versteckte Beiträge werden beim Auto-Purge entfernt. Min 1 Tag, max 365 Tage. +

+
+ +
+ +
+
+ + diff --git a/web/settings.css b/web/settings.css index 9c52bb3..35e0302 100644 --- a/web/settings.css +++ b/web/settings.css @@ -112,6 +112,73 @@ border-top: 1px solid #e4e6eb; } +.floating-save-btn { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 20; + padding: 14px 22px; + border-radius: 999px; + font-size: 15px; + font-weight: 700; + color: #fff; + background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); + border: none; + box-shadow: 0 10px 30px rgba(37, 99, 235, 0.35); + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.2s ease; + min-width: 210px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.floating-save-btn:hover { + transform: translateY(-1px); + box-shadow: 0 14px 34px rgba(37, 99, 235, 0.4); +} + +.floating-save-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); +} + +.floating-save-btn:focus { + outline: 3px solid rgba(37, 99, 235, 0.35); + outline-offset: 3px; +} + +.floating-save-btn .spinner { + font-size: 16px; + line-height: 1; +} + +.floating-save-btn.is-saving { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); +} + +.floating-save-btn .spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@media (max-width: 640px) { + .floating-save-btn { + left: 16px; + right: 16px; + bottom: 16px; + width: auto; + justify-content: center; + } +} + /* Credentials List */ .credentials-list { diff --git a/web/settings.js b/web/settings.js index 9ac1f5e..1b23161 100644 --- a/web/settings.js +++ b/web/settings.js @@ -43,6 +43,7 @@ const PROVIDER_INFO = { let credentials = []; let currentSettings = null; +let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 }; function apiFetch(url, options = {}) { return fetch(url, {...options, credentials: 'include'}); @@ -52,7 +53,7 @@ function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; - bottom: 24px; + bottom: 84px; right: 24px; background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'}; color: white; @@ -64,6 +65,7 @@ function showToast(message, type = 'info') { z-index: 999999; max-width: 350px; animation: slideIn 0.3s ease-out; + pointer-events: none; `; toast.textContent = message; @@ -124,6 +126,93 @@ async function loadSettings() { '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 shorten(text, maxLength = 80) { if (typeof text !== 'string') { return ''; @@ -626,8 +715,10 @@ async function saveCredentialOrder() { } } -async function saveSettings(e) { - e.preventDefault(); +async function saveSettings(e, { silent = false } = {}) { + if (e && typeof e.preventDefault === 'function') { + e.preventDefault(); + } try { const data = { @@ -648,9 +739,15 @@ async function saveSettings(e) { } currentSettings = await res.json(); - showSuccess('✅ Einstellungen erfolgreich gespeichert'); + if (!silent) { + showSuccess('✅ Einstellungen erfolgreich gespeichert'); + } + return true; } catch (err) { - showError('❌ ' + err.message); + if (!silent) { + showError('❌ ' + err.message); + } + return false; } } @@ -720,6 +817,64 @@ function escapeHtml(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 }), + 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 // ============================================================================ @@ -755,7 +910,7 @@ async function loadProfileFriends() { } } -async function saveFriends(profileNumber, friendNames) { +async function saveFriends(profileNumber, friendNames, { silent = false } = {}) { try { const res = await apiFetch(`${API_URL}/profile-friends/${profileNumber}`, { method: 'PUT', @@ -769,12 +924,34 @@ async function saveFriends(profileNumber, friendNames) { } profileFriends[profileNumber] = friendNames; - showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`); + if (!silent) { + showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`); + } + return true; } catch (err) { - showError('❌ ' + err.message); + 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); @@ -785,14 +962,22 @@ document.getElementById('aiSettingsForm').addEventListener('submit', (e) => { e.preventDefault(); saveSettings(e); }); -document.getElementById('saveAllBtn').addEventListener('click', (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; + } + }); +} // Initialize -Promise.all([loadCredentials(), loadSettings(), loadProfileFriends()]).catch(err => showError(err.message)); +Promise.all([loadCredentials(), loadSettings(), loadHiddenSettings(), loadProfileFriends()]).catch(err => showError(err.message)); })();