reworked settings page

This commit is contained in:
2025-11-21 14:33:34 +01:00
parent 23a5714119
commit 2e4a6ae7c4
4 changed files with 403 additions and 29 deletions

View File

@@ -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(` db.exec(`
CREATE INDEX IF NOT EXISTS idx_search_seen_posts_last_seen_at CREATE INDEX IF NOT EXISTS idx_search_seen_posts_last_seen_at
ON search_seen_posts(last_seen_at); ON search_seen_posts(last_seen_at);
@@ -1546,9 +1555,62 @@ function getFormattedCredentialById(id) {
return formatCredentialRow(row[0]); 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() { function cleanupExpiredSearchPosts() {
try { 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(` db.prepare(`
DELETE FROM search_seen_posts DELETE FROM search_seen_posts
WHERE last_seen_at < DATETIME('now', ?) 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) { function sanitizeAIComment(text) {
if (!text) { if (!text) {
return ''; return '';

View File

@@ -428,6 +428,16 @@
</button> </button>
</section> </section>
<!-- Profile Friends Section -->
<section class="settings-section">
<h2 class="section-title">👥 Freundesnamen pro Profil</h2>
<p class="section-description">
Gib für jedes Profil eine Liste von Freundesnamen an, die im Prompt verwendet werden können.
</p>
<div id="profileFriendsList"></div>
</section>
<!-- AI Settings Section --> <!-- AI Settings Section -->
<section class="settings-section"> <section class="settings-section">
<h2 class="section-title">AI-Kommentar-Generator</h2> <h2 class="section-title">AI-Kommentar-Generator</h2>
@@ -461,32 +471,55 @@
Dieser Text wird vor dem eigentlichen Post-Text an die KI gesendet. Verwende <code>{FREUNDE}</code> als Platzhalter für Freundesnamen. Dieser Text wird vor dem eigentlichen Post-Text an die KI gesendet. Verwende <code>{FREUNDE}</code> als Platzhalter für Freundesnamen.
</p> </p>
</div> </div>
</form>
</section>
<!-- Profile Friends Section -->
<section class="settings-section">
<h2 class="section-title">👥 Freundesnamen pro Profil</h2>
<p class="section-description">
Gib für jedes Profil eine Liste von Freundesnamen an, die im Prompt verwendet werden können.
</p>
<div id="profileFriendsList"></div>
</section>
<!-- Save Button at the end -->
<section class="settings-section">
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-primary" id="saveAllBtn">
💾 Einstellungen speichern
</button>
<button type="button" class="btn btn-secondary" id="testBtn"> <button type="button" class="btn btn-secondary" id="testBtn">
🧪 Kommentar testen 🧪 Kommentar testen
</button> </button>
</div> </div>
</form>
</section>
<!-- Hidden posts / purge settings -->
<section class="settings-section">
<h2 class="section-title">Versteckte Beiträge bereinigen</h2>
<p class="section-description">
Steuere die automatische Bereinigung versteckter/ausgeblendeter Beiträge aus der Suche und starte bei Bedarf eine manuelle Bereinigung.
</p>
<form id="hiddenSettingsForm">
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="autoPurgeHiddenToggle" class="form-checkbox">
<span>Automatisches Purging aktivieren</span>
</label>
<p class="form-help">
Löscht versteckte Beiträge nach Ablauf der Aufbewahrungsdauer automatisch.
</p>
</div>
<div class="form-group">
<label for="hiddenRetentionDays" class="form-label">Aufbewahrungsdauer (Tage)</label>
<input type="number" id="hiddenRetentionDays" class="form-input" min="1" max="365" step="1" value="90">
<p class="form-help">
Ältere versteckte Beiträge werden beim Auto-Purge entfernt. Min 1 Tag, max 365 Tage.
</p>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="purgeHiddenNowBtn">
🧹 Jetzt bereinigen
</button>
</div>
</form>
</section> </section>
</div> </div>
<button type="button" class="floating-save-btn" id="saveAllFloatingBtn" aria-label="Einstellungen speichern">
<span class="spinner" aria-hidden="true" style="display: none;"></span>
<span class="label">Einstellungen speichern</span>
</button>
</div> </div>
<!-- Add/Edit Credential Modal --> <!-- Add/Edit Credential Modal -->

View File

@@ -112,6 +112,73 @@
border-top: 1px solid #e4e6eb; 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 */
.credentials-list { .credentials-list {

View File

@@ -43,6 +43,7 @@ const PROVIDER_INFO = {
let credentials = []; let credentials = [];
let currentSettings = null; let currentSettings = null;
let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 };
function apiFetch(url, options = {}) { function apiFetch(url, options = {}) {
return fetch(url, {...options, credentials: 'include'}); return fetch(url, {...options, credentials: 'include'});
@@ -52,7 +53,7 @@ function showToast(message, type = 'info') {
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.style.cssText = ` toast.style.cssText = `
position: fixed; position: fixed;
bottom: 24px; bottom: 84px;
right: 24px; right: 24px;
background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'}; background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'};
color: white; color: white;
@@ -64,6 +65,7 @@ function showToast(message, type = 'info') {
z-index: 999999; z-index: 999999;
max-width: 350px; max-width: 350px;
animation: slideIn 0.3s ease-out; animation: slideIn 0.3s ease-out;
pointer-events: none;
`; `;
toast.textContent = message; 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'; '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) { function shorten(text, maxLength = 80) {
if (typeof text !== 'string') { if (typeof text !== 'string') {
return ''; return '';
@@ -626,8 +715,10 @@ async function saveCredentialOrder() {
} }
} }
async function saveSettings(e) { async function saveSettings(e, { silent = false } = {}) {
if (e && typeof e.preventDefault === 'function') {
e.preventDefault(); e.preventDefault();
}
try { try {
const data = { const data = {
@@ -648,10 +739,16 @@ async function saveSettings(e) {
} }
currentSettings = await res.json(); currentSettings = await res.json();
if (!silent) {
showSuccess('✅ Einstellungen erfolgreich gespeichert'); showSuccess('✅ Einstellungen erfolgreich gespeichert');
}
return true;
} catch (err) { } catch (err) {
if (!silent) {
showError('❌ ' + err.message); showError('❌ ' + err.message);
} }
return false;
}
} }
async function testComment() { async function testComment() {
@@ -720,6 +817,64 @@ function escapeHtml(text) {
return div.innerHTML; 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 // PROFILE FRIENDS
// ============================================================================ // ============================================================================
@@ -755,7 +910,7 @@ async function loadProfileFriends() {
} }
} }
async function saveFriends(profileNumber, friendNames) { async function saveFriends(profileNumber, friendNames, { silent = false } = {}) {
try { try {
const res = await apiFetch(`${API_URL}/profile-friends/${profileNumber}`, { const res = await apiFetch(`${API_URL}/profile-friends/${profileNumber}`, {
method: 'PUT', method: 'PUT',
@@ -769,10 +924,32 @@ async function saveFriends(profileNumber, friendNames) {
} }
profileFriends[profileNumber] = friendNames; profileFriends[profileNumber] = friendNames;
if (!silent) {
showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`); showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`);
}
return true;
} catch (err) { } catch (err) {
if (!silent) {
showError('❌ ' + err.message); 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 // Event listeners
@@ -785,14 +962,22 @@ document.getElementById('aiSettingsForm').addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
saveSettings(e); saveSettings(e);
}); });
document.getElementById('saveAllBtn').addEventListener('click', (e) => {
e.preventDefault();
saveSettings(e);
});
document.getElementById('testBtn').addEventListener('click', testComment); document.getElementById('testBtn').addEventListener('click', testComment);
document.getElementById('testModalClose').addEventListener('click', () => document.getElementById('testModal').setAttribute('hidden', '')); document.getElementById('testModalClose').addEventListener('click', () => document.getElementById('testModal').setAttribute('hidden', ''));
document.getElementById('generateTestComment').addEventListener('click', generateTest); 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 // Initialize
Promise.all([loadCredentials(), loadSettings(), loadProfileFriends()]).catch(err => showError(err.message)); Promise.all([loadCredentials(), loadSettings(), loadHiddenSettings(), loadProfileFriends()]).catch(err => showError(err.message));
})(); })();