reworked settings page
This commit is contained in:
@@ -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 '';
|
||||
|
||||
@@ -428,6 +428,16 @@
|
||||
</button>
|
||||
</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 -->
|
||||
<section class="settings-section">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="testBtn">
|
||||
🧪 Kommentar testen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Profile Friends Section -->
|
||||
<!-- Hidden posts / purge settings -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">👥 Freundesnamen pro Profil</h2>
|
||||
<h2 class="section-title">Versteckte Beiträge bereinigen</h2>
|
||||
<p class="section-description">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div id="profileFriendsList"></div>
|
||||
</section>
|
||||
<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>
|
||||
|
||||
<!-- Save Button at the end -->
|
||||
<section class="settings-section">
|
||||
<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">
|
||||
🧪 Kommentar testen
|
||||
</button>
|
||||
</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>
|
||||
|
||||
</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>
|
||||
|
||||
<!-- Add/Edit Credential Modal -->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
211
web/settings.js
211
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));
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user