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.
+
+
+
+ 🧪 Kommentar testen
+
+
-
+
- 👥 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.
-
-
+
+
+
+ ⏳
+ Einstellungen speichern
+
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));
})();