Add support for disabling broken profiles
This commit is contained in:
@@ -31,6 +31,9 @@ const DEFAULT_PROFILE_NAMES = {
|
|||||||
4: 'Profil 4',
|
4: 'Profil 4',
|
||||||
5: 'Profil 5'
|
5: 'Profil 5'
|
||||||
};
|
};
|
||||||
|
const DEFAULT_ACTIVE_PROFILES = Object.freeze(
|
||||||
|
Array.from({ length: MAX_PROFILES }, (_unused, index) => index + 1)
|
||||||
|
);
|
||||||
const PROFILE_SCOPE_COOKIE = 'fb_tracker_scope';
|
const PROFILE_SCOPE_COOKIE = 'fb_tracker_scope';
|
||||||
const PROFILE_SCOPE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
|
const PROFILE_SCOPE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
|
||||||
const FACEBOOK_TRACKING_PARAM_PREFIXES = ['__cft__', '__tn__', '__eep__', 'mibextid'];
|
const FACEBOOK_TRACKING_PARAM_PREFIXES = ['__cft__', '__tn__', '__eep__', 'mibextid'];
|
||||||
@@ -687,17 +690,27 @@ function setScopedProfileNumber(scopeId, profileNumber) {
|
|||||||
`).run(scopeId, profileNumber);
|
`).run(scopeId, profileNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampTargetCount(value) {
|
function normalizeProfileLimit(value) {
|
||||||
|
const parsed = parseInt(value, 10);
|
||||||
|
if (Number.isNaN(parsed) || parsed < 1) {
|
||||||
|
return MAX_PROFILES;
|
||||||
|
}
|
||||||
|
return Math.min(MAX_PROFILES, parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampTargetCount(value, maxProfiles = MAX_PROFILES) {
|
||||||
|
const maxAllowed = normalizeProfileLimit(maxProfiles);
|
||||||
const parsed = parseInt(value, 10);
|
const parsed = parseInt(value, 10);
|
||||||
if (Number.isNaN(parsed)) {
|
if (Number.isNaN(parsed)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return Math.min(MAX_PROFILES, Math.max(1, parsed));
|
return Math.min(maxAllowed, Math.max(1, parsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTargetCount(value) {
|
function validateTargetCount(value, maxProfiles = MAX_PROFILES) {
|
||||||
|
const maxAllowed = normalizeProfileLimit(maxProfiles);
|
||||||
const parsed = parseInt(value, 10);
|
const parsed = parseInt(value, 10);
|
||||||
if (Number.isNaN(parsed) || parsed < 1 || parsed > MAX_PROFILES) {
|
if (Number.isNaN(parsed) || parsed < 1 || parsed > maxAllowed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
@@ -711,6 +724,120 @@ function sanitizeProfileNumber(value) {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProfileNumberList(raw) {
|
||||||
|
let source = raw;
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
const trimmed = source.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
source = JSON.parse(trimmed);
|
||||||
|
} catch (error) {
|
||||||
|
source = trimmed.split(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(source)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const profiles = [];
|
||||||
|
source.forEach((value) => {
|
||||||
|
const profileNumber = sanitizeProfileNumber(value);
|
||||||
|
if (!profileNumber || seen.has(profileNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(profileNumber);
|
||||||
|
profiles.push(profileNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
return profiles.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultActiveProfiles() {
|
||||||
|
return [...DEFAULT_ACTIVE_PROFILES];
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadProfileSettings() {
|
||||||
|
let settings = db.prepare('SELECT * FROM profile_settings WHERE id = 1').get();
|
||||||
|
let activeProfiles = settings ? normalizeProfileNumberList(settings.active_profiles) : [];
|
||||||
|
|
||||||
|
if (!activeProfiles.length) {
|
||||||
|
activeProfiles = getDefaultActiveProfiles();
|
||||||
|
const serialized = JSON.stringify(activeProfiles);
|
||||||
|
const existing = db.prepare('SELECT id FROM profile_settings WHERE id = 1').get();
|
||||||
|
if (existing) {
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE profile_settings
|
||||||
|
SET active_profiles = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = 1
|
||||||
|
`).run(serialized);
|
||||||
|
} else {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO profile_settings (id, active_profiles, updated_at)
|
||||||
|
VALUES (1, ?, CURRENT_TIMESTAMP)
|
||||||
|
`).run(serialized);
|
||||||
|
}
|
||||||
|
settings = db.prepare('SELECT * FROM profile_settings WHERE id = 1').get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const inactiveProfiles = getDefaultActiveProfiles().filter(profileNumber => !activeProfiles.includes(profileNumber));
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated_at: settings && settings.updated_at ? settings.updated_at : null,
|
||||||
|
active_profiles: activeProfiles,
|
||||||
|
inactive_profiles: inactiveProfiles,
|
||||||
|
max_target_count: activeProfiles.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveProfileSettings(input = {}) {
|
||||||
|
const activeProfiles = normalizeProfileNumberList(input.active_profiles);
|
||||||
|
if (!activeProfiles.length) {
|
||||||
|
throw new Error('Mindestens ein Profil muss aktiv bleiben.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(activeProfiles);
|
||||||
|
const existing = db.prepare('SELECT id FROM profile_settings WHERE id = 1').get();
|
||||||
|
if (existing) {
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE profile_settings
|
||||||
|
SET active_profiles = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = 1
|
||||||
|
`).run(serialized);
|
||||||
|
} else {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO profile_settings (id, active_profiles, updated_at)
|
||||||
|
VALUES (1, ?, CURRENT_TIMESTAMP)
|
||||||
|
`).run(serialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadProfileSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveProfiles(settings = null) {
|
||||||
|
const source = settings && Array.isArray(settings.active_profiles)
|
||||||
|
? settings.active_profiles
|
||||||
|
: loadProfileSettings().active_profiles;
|
||||||
|
const activeProfiles = normalizeProfileNumberList(source);
|
||||||
|
return activeProfiles.length ? activeProfiles : getDefaultActiveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveProfileNumber(profileNumber, settings = null) {
|
||||||
|
const sanitized = sanitizeProfileNumber(profileNumber);
|
||||||
|
if (!sanitized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getActiveProfiles(settings).includes(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectableScopedProfileNumber(scopeId, settings = null) {
|
||||||
|
const profileNumber = getScopedProfileNumber(scopeId);
|
||||||
|
return isActiveProfileNumber(profileNumber, settings) ? profileNumber : null;
|
||||||
|
}
|
||||||
|
|
||||||
function applyProfileVariantTemplates(text, profileNumber) {
|
function applyProfileVariantTemplates(text, profileNumber) {
|
||||||
if (typeof text !== 'string' || !text) {
|
if (typeof text !== 'string' || !text) {
|
||||||
return text || '';
|
return text || '';
|
||||||
@@ -1950,8 +2077,10 @@ function extractFacebookContentKey(normalizedUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRequiredProfiles(targetCount) {
|
function getRequiredProfiles(targetCount) {
|
||||||
const count = clampTargetCount(targetCount);
|
const profileSettings = loadProfileSettings();
|
||||||
return Array.from({ length: count }, (_, index) => index + 1);
|
const activeProfiles = getActiveProfiles(profileSettings);
|
||||||
|
const count = clampTargetCount(targetCount, activeProfiles.length);
|
||||||
|
return activeProfiles.slice(0, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProfileStatuses(requiredProfiles, checks) {
|
function buildProfileStatuses(requiredProfiles, checks) {
|
||||||
@@ -2004,11 +2133,14 @@ function buildCompletedProfileSet(rows) {
|
|||||||
return completedSet;
|
return completedSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
function countUniqueProfileChecks(checks) {
|
function countUniqueProfileChecks(checks, allowedProfiles = null) {
|
||||||
|
const allowedSet = Array.isArray(allowedProfiles) && allowedProfiles.length
|
||||||
|
? new Set(allowedProfiles)
|
||||||
|
: null;
|
||||||
const uniqueProfiles = new Set();
|
const uniqueProfiles = new Set();
|
||||||
checks.forEach((check) => {
|
checks.forEach((check) => {
|
||||||
const profileNumber = sanitizeProfileNumber(check.profile_number);
|
const profileNumber = sanitizeProfileNumber(check.profile_number);
|
||||||
if (profileNumber) {
|
if (profileNumber && (!allowedSet || allowedSet.has(profileNumber))) {
|
||||||
uniqueProfiles.add(profileNumber);
|
uniqueProfiles.add(profileNumber);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2039,7 +2171,7 @@ function recalcCheckedCount(postId) {
|
|||||||
const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ?').all(postId);
|
const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ?').all(postId);
|
||||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||||
const { statuses } = buildProfileStatuses(requiredProfiles, checks);
|
const { statuses } = buildProfileStatuses(requiredProfiles, checks);
|
||||||
const checkedCount = Math.min(requiredProfiles.length, countUniqueProfileChecks(checks));
|
const checkedCount = Math.min(requiredProfiles.length, countUniqueProfileChecks(checks, requiredProfiles));
|
||||||
|
|
||||||
const updates = [];
|
const updates = [];
|
||||||
const params = [];
|
const params = [];
|
||||||
@@ -2170,6 +2302,14 @@ db.exec(`
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS profile_settings (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
active_profiles TEXT,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS ai_auto_comment_rate_limit_settings (
|
CREATE TABLE IF NOT EXISTS ai_auto_comment_rate_limit_settings (
|
||||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
@@ -2678,6 +2818,7 @@ ensureColumn('posts', 'is_successful', 'is_successful INTEGER DEFAULT 0');
|
|||||||
ensureColumn('ai_settings', 'active_credential_id', 'active_credential_id INTEGER');
|
ensureColumn('ai_settings', 'active_credential_id', 'active_credential_id INTEGER');
|
||||||
ensureColumn('ai_settings', 'prompt_prefix', 'prompt_prefix TEXT');
|
ensureColumn('ai_settings', 'prompt_prefix', 'prompt_prefix TEXT');
|
||||||
ensureColumn('ai_settings', 'enabled', 'enabled INTEGER DEFAULT 0');
|
ensureColumn('ai_settings', 'enabled', 'enabled INTEGER DEFAULT 0');
|
||||||
|
ensureColumn('profile_settings', 'active_profiles', 'active_profiles TEXT');
|
||||||
ensureColumn('ai_auto_comment_rate_limit_settings', 'enabled', 'enabled INTEGER NOT NULL DEFAULT 1');
|
ensureColumn('ai_auto_comment_rate_limit_settings', 'enabled', 'enabled INTEGER NOT NULL DEFAULT 1');
|
||||||
ensureColumn('ai_auto_comment_rate_limit_settings', 'requests_per_minute', 'requests_per_minute INTEGER NOT NULL DEFAULT 2');
|
ensureColumn('ai_auto_comment_rate_limit_settings', 'requests_per_minute', 'requests_per_minute INTEGER NOT NULL DEFAULT 2');
|
||||||
ensureColumn('ai_auto_comment_rate_limit_settings', 'requests_per_hour', 'requests_per_hour INTEGER NOT NULL DEFAULT 20');
|
ensureColumn('ai_auto_comment_rate_limit_settings', 'requests_per_hour', 'requests_per_hour INTEGER NOT NULL DEFAULT 20');
|
||||||
@@ -4905,7 +5046,7 @@ function mapPostRow(post) {
|
|||||||
const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ? ORDER BY checked_at ASC').all(post.id);
|
const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ? ORDER BY checked_at ASC').all(post.id);
|
||||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||||
const { statuses, completedChecks } = buildProfileStatuses(requiredProfiles, checks);
|
const { statuses, completedChecks } = buildProfileStatuses(requiredProfiles, checks);
|
||||||
const checkedCount = Math.min(requiredProfiles.length, countUniqueProfileChecks(checks));
|
const checkedCount = Math.min(requiredProfiles.length, countUniqueProfileChecks(checks, requiredProfiles));
|
||||||
const screenshotFile = post.screenshot_path ? path.join(screenshotDir, post.screenshot_path) : null;
|
const screenshotFile = post.screenshot_path ? path.join(screenshotDir, post.screenshot_path) : null;
|
||||||
const screenshotPath = screenshotFile && fs.existsSync(screenshotFile)
|
const screenshotPath = screenshotFile && fs.existsSync(screenshotFile)
|
||||||
? `/api/posts/${post.id}/screenshot`
|
? `/api/posts/${post.id}/screenshot`
|
||||||
@@ -6146,10 +6287,34 @@ app.delete('/api/search-posts', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/profile-settings', (req, res) => {
|
||||||
|
try {
|
||||||
|
const settings = loadProfileSettings();
|
||||||
|
res.json(settings);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/profile-settings', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { active_profiles } = req.body || {};
|
||||||
|
if (!Array.isArray(active_profiles)) {
|
||||||
|
return res.status(400).json({ error: 'active_profiles must be an array' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = saveProfileSettings({ active_profiles });
|
||||||
|
res.json(saved);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/profile-state', (req, res) => {
|
app.get('/api/profile-state', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scopeId = req.profileScope;
|
const scopeId = req.profileScope;
|
||||||
const profileNumber = getScopedProfileNumber(scopeId);
|
const profileSettings = loadProfileSettings();
|
||||||
|
const profileNumber = getSelectableScopedProfileNumber(scopeId, profileSettings);
|
||||||
res.json({ profile_number: profileNumber || null });
|
res.json({ profile_number: profileNumber || null });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -6163,13 +6328,13 @@ app.post('/api/profile-state', (req, res) => {
|
|||||||
return res.status(400).json({ error: 'profile_number is required' });
|
return res.status(400).json({ error: 'profile_number is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parseInt(profile_number, 10);
|
const profileSettings = loadProfileSettings();
|
||||||
if (Number.isNaN(parsed) || parsed < 1 || parsed > 5) {
|
const sanitized = sanitizeProfileNumber(profile_number);
|
||||||
return res.status(400).json({ error: 'profile_number must be between 1 and 5' });
|
if (!sanitized || !isActiveProfileNumber(sanitized, profileSettings)) {
|
||||||
|
return res.status(400).json({ error: 'profile_number must reference an active profile' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const scopeId = req.profileScope;
|
const scopeId = req.profileScope;
|
||||||
const sanitized = sanitizeProfileNumber(parsed) || 1;
|
|
||||||
setScopedProfileNumber(scopeId, sanitized);
|
setScopedProfileNumber(scopeId, sanitized);
|
||||||
|
|
||||||
res.json({ profile_number: sanitized });
|
res.json({ profile_number: sanitized });
|
||||||
@@ -6277,7 +6442,11 @@ app.post('/api/posts', (req, res) => {
|
|||||||
post_text
|
post_text
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const validatedTargetCount = validateTargetCount(typeof target_count === 'undefined' ? 1 : target_count);
|
const activeProfiles = getActiveProfiles();
|
||||||
|
const validatedTargetCount = validateTargetCount(
|
||||||
|
typeof target_count === 'undefined' ? 1 : target_count,
|
||||||
|
activeProfiles.length
|
||||||
|
);
|
||||||
const alternateUrlsInput = Array.isArray(req.body.alternate_urls) ? req.body.alternate_urls : [];
|
const alternateUrlsInput = Array.isArray(req.body.alternate_urls) ? req.body.alternate_urls : [];
|
||||||
|
|
||||||
const normalizedUrl = normalizeFacebookPostUrl(url);
|
const normalizedUrl = normalizeFacebookPostUrl(url);
|
||||||
@@ -6287,7 +6456,7 @@ app.post('/api/posts', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!validatedTargetCount) {
|
if (!validatedTargetCount) {
|
||||||
return res.status(400).json({ error: 'target_count must be between 1 and 5' });
|
return res.status(400).json({ error: `target_count must be between 1 and ${activeProfiles.length}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
@@ -6417,11 +6586,12 @@ app.put('/api/posts/:postId', (req, res) => {
|
|||||||
const params = [];
|
const params = [];
|
||||||
let normalizedUrlForCleanup = null;
|
let normalizedUrlForCleanup = null;
|
||||||
let updatedContentKey = null;
|
let updatedContentKey = null;
|
||||||
|
const activeProfiles = getActiveProfiles();
|
||||||
|
|
||||||
if (typeof target_count !== 'undefined') {
|
if (typeof target_count !== 'undefined') {
|
||||||
const validatedTargetCount = validateTargetCount(target_count);
|
const validatedTargetCount = validateTargetCount(target_count, activeProfiles.length);
|
||||||
if (!validatedTargetCount) {
|
if (!validatedTargetCount) {
|
||||||
return res.status(400).json({ error: 'target_count must be between 1 and 5' });
|
return res.status(400).json({ error: `target_count must be between 1 and ${activeProfiles.length}` });
|
||||||
}
|
}
|
||||||
updates.push('target_count = ?');
|
updates.push('target_count = ?');
|
||||||
params.push(validatedTargetCount);
|
params.push(validatedTargetCount);
|
||||||
@@ -6546,6 +6716,7 @@ app.post('/api/posts/:postId/check', (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const profileSettings = loadProfileSettings();
|
||||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
if (post.target_count !== requiredProfiles.length) {
|
if (post.target_count !== requiredProfiles.length) {
|
||||||
@@ -6554,11 +6725,18 @@ app.post('/api/posts/:postId/check', (req, res) => {
|
|||||||
didChange = true;
|
didChange = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let profileValue = sanitizeProfileNumber(profile_number) || getScopedProfileNumber(req.profileScope);
|
let profileValue = sanitizeProfileNumber(profile_number);
|
||||||
|
if (!profileValue) {
|
||||||
|
profileValue = getSelectableScopedProfileNumber(req.profileScope, profileSettings);
|
||||||
|
}
|
||||||
if (!profileValue) {
|
if (!profileValue) {
|
||||||
return res.status(400).json({ error: 'Profil muss zuerst ausgewählt werden.' });
|
return res.status(400).json({ error: 'Profil muss zuerst ausgewählt werden.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!requiredProfiles.includes(profileValue)) {
|
||||||
|
return res.status(409).json({ error: 'Dieses Profil ist aktuell deaktiviert oder für diesen Beitrag nicht erforderlich.' });
|
||||||
|
}
|
||||||
|
|
||||||
const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(postId);
|
const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(postId);
|
||||||
const completedSet = buildCompletedProfileSet(completedRows);
|
const completedSet = buildCompletedProfileSet(completedRows);
|
||||||
const shouldEnforceOrder = shouldEnforceProfileOrder({
|
const shouldEnforceOrder = shouldEnforceProfileOrder({
|
||||||
@@ -6696,6 +6874,7 @@ app.post('/api/check-by-url', (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const profileSettings = loadProfileSettings();
|
||||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
if (post.target_count !== requiredProfiles.length) {
|
if (post.target_count !== requiredProfiles.length) {
|
||||||
@@ -6704,11 +6883,18 @@ app.post('/api/check-by-url', (req, res) => {
|
|||||||
didChange = true;
|
didChange = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let profileValue = sanitizeProfileNumber(profile_number) || getScopedProfileNumber(req.profileScope);
|
let profileValue = sanitizeProfileNumber(profile_number);
|
||||||
|
if (!profileValue) {
|
||||||
|
profileValue = getSelectableScopedProfileNumber(req.profileScope, profileSettings);
|
||||||
|
}
|
||||||
if (!profileValue) {
|
if (!profileValue) {
|
||||||
return res.status(400).json({ error: 'Profil muss zuerst ausgewählt werden.' });
|
return res.status(400).json({ error: 'Profil muss zuerst ausgewählt werden.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!requiredProfiles.includes(profileValue)) {
|
||||||
|
return res.status(409).json({ error: 'Dieses Profil ist aktuell deaktiviert oder für diesen Beitrag nicht erforderlich.' });
|
||||||
|
}
|
||||||
|
|
||||||
const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(post.id);
|
const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(post.id);
|
||||||
const completedSet = buildCompletedProfileSet(completedRows);
|
const completedSet = buildCompletedProfileSet(completedRows);
|
||||||
const shouldEnforceOrder = shouldEnforceProfileOrder({
|
const shouldEnforceOrder = shouldEnforceProfileOrder({
|
||||||
@@ -6790,7 +6976,7 @@ app.post('/api/posts/:postId/profile-status', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!requiredProfiles.includes(profileValue)) {
|
if (!requiredProfiles.includes(profileValue)) {
|
||||||
return res.status(409).json({ error: 'Dieses Profil ist für diesen Beitrag nicht erforderlich.' });
|
return res.status(409).json({ error: 'Dieses Profil ist aktuell deaktiviert oder für diesen Beitrag nicht erforderlich.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedStatus = status === 'done' ? 'done' : 'pending';
|
const normalizedStatus = status === 'done' ? 'done' : 'pending';
|
||||||
@@ -6967,9 +7153,10 @@ app.post('/api/posts/merge', (req, res) => {
|
|||||||
return primaryPost.deadline_at || secondaryPost.deadline_at || null;
|
return primaryPost.deadline_at || secondaryPost.deadline_at || null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const activeProfileLimit = getActiveProfiles().length;
|
||||||
const mergedTargetCount = Math.max(
|
const mergedTargetCount = Math.max(
|
||||||
validateTargetCount(primaryPost.target_count) || 1,
|
clampTargetCount(primaryPost.target_count, activeProfileLimit),
|
||||||
validateTargetCount(secondaryPost.target_count) || 1
|
clampTargetCount(secondaryPost.target_count, activeProfileLimit)
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergedPostText = (primaryPost.post_text && primaryPost.post_text.trim())
|
const mergedPostText = (primaryPost.post_text && primaryPost.post_text.trim())
|
||||||
|
|||||||
171
web/app.js
171
web/app.js
@@ -31,6 +31,7 @@ let updatesStreamHealthy = false;
|
|||||||
let updatesShouldResyncOnConnect = false;
|
let updatesShouldResyncOnConnect = false;
|
||||||
|
|
||||||
const MAX_PROFILES = 5;
|
const MAX_PROFILES = 5;
|
||||||
|
const DEFAULT_ACTIVE_PROFILES = Array.from({ length: MAX_PROFILES }, (_unused, index) => index + 1);
|
||||||
const PROFILE_NAMES = {
|
const PROFILE_NAMES = {
|
||||||
1: 'Profil 1',
|
1: 'Profil 1',
|
||||||
2: 'Profil 2',
|
2: 'Profil 2',
|
||||||
@@ -38,11 +39,42 @@ const PROFILE_NAMES = {
|
|||||||
4: 'Profil 4',
|
4: 'Profil 4',
|
||||||
5: 'Profil 5'
|
5: 'Profil 5'
|
||||||
};
|
};
|
||||||
|
let activeProfileNumbers = [...DEFAULT_ACTIVE_PROFILES];
|
||||||
|
|
||||||
function isValidProfileNumber(value) {
|
function isValidProfileNumber(value) {
|
||||||
return Number.isInteger(value) && value >= 1 && value <= MAX_PROFILES;
|
return Number.isInteger(value) && value >= 1 && value <= MAX_PROFILES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeActiveProfileNumbers(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
return [...DEFAULT_ACTIVE_PROFILES];
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const normalized = values
|
||||||
|
.map((value) => parseInt(value, 10))
|
||||||
|
.filter((value) => isValidProfileNumber(value) && !seen.has(value) && seen.add(value))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
return normalized.length ? normalized : [...DEFAULT_ACTIVE_PROFILES];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveProfileNumbers() {
|
||||||
|
return activeProfileNumbers.length ? [...activeProfileNumbers] : [...DEFAULT_ACTIVE_PROFILES];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelectableProfileNumber(value) {
|
||||||
|
return isValidProfileNumber(value) && getActiveProfileNumbers().includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveTargetCountLimit() {
|
||||||
|
return Math.max(1, getActiveProfileNumbers().length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetCountErrorMessage() {
|
||||||
|
return `Die Anzahl der benötigten Profile muss zwischen 1 und ${getActiveTargetCountLimit()} liegen.`;
|
||||||
|
}
|
||||||
|
|
||||||
function redirectToLogin() {
|
function redirectToLogin() {
|
||||||
try {
|
try {
|
||||||
const redirect = encodeURIComponent(window.location.href);
|
const redirect = encodeURIComponent(window.location.href);
|
||||||
@@ -2142,11 +2174,12 @@ function normalizeRequiredProfiles(post) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedTarget = parseInt(post.target_count, 10);
|
const parsedTarget = parseInt(post.target_count, 10);
|
||||||
|
const activeProfiles = getActiveProfileNumbers();
|
||||||
const count = Number.isNaN(parsedTarget)
|
const count = Number.isNaN(parsedTarget)
|
||||||
? 1
|
? 1
|
||||||
: Math.min(MAX_PROFILES, Math.max(1, parsedTarget));
|
: Math.min(activeProfiles.length, Math.max(1, parsedTarget));
|
||||||
|
|
||||||
return Array.from({ length: count }, (_, index) => index + 1);
|
return activeProfiles.slice(0, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabButtons() {
|
function getTabButtons() {
|
||||||
@@ -3510,7 +3543,7 @@ function applyAutoRefreshSettings() {
|
|||||||
autoRefreshTimer = null;
|
autoRefreshTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidProfileNumber(currentProfile)) {
|
if (!isSelectableProfileNumber(currentProfile)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3534,7 +3567,7 @@ function applyAutoRefreshSettings() {
|
|||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isValidProfileNumber(currentProfile)) {
|
if (!isSelectableProfileNumber(currentProfile)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetchPosts({ showLoader: false });
|
fetchPosts({ showLoader: false });
|
||||||
@@ -3782,6 +3815,90 @@ function applyScreenshotModalSize() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderProfileSelectorOptions() {
|
||||||
|
if (!profileSelectElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProfiles = getActiveProfileNumbers();
|
||||||
|
profileSelectElement.innerHTML = `
|
||||||
|
<option value="">-- Profil wählen --</option>
|
||||||
|
${activeProfiles.map((profileNumber) => `
|
||||||
|
<option value="${profileNumber}">${getProfileName(profileNumber)}</option>
|
||||||
|
`).join('')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (isSelectableProfileNumber(currentProfile)) {
|
||||||
|
profileSelectElement.value = String(currentProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderManualTargetOptions(selectedValue = 1) {
|
||||||
|
if (!manualPostTargetSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxCount = getActiveTargetCountLimit();
|
||||||
|
const normalizedSelected = Math.max(1, Math.min(maxCount, parseInt(selectedValue, 10) || 1));
|
||||||
|
manualPostTargetSelect.innerHTML = Array.from({ length: maxCount }, (_unused, index) => {
|
||||||
|
const value = index + 1;
|
||||||
|
return `<option value="${value}">${value}</option>`;
|
||||||
|
}).join('');
|
||||||
|
manualPostTargetSelect.value = String(normalizedSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectedProfile(message = 'Bitte zuerst ein Profil auswählen.') {
|
||||||
|
if (profileSelectElement) {
|
||||||
|
profileSelectElement.value = '';
|
||||||
|
}
|
||||||
|
currentProfile = null;
|
||||||
|
try {
|
||||||
|
localStorage.removeItem('profileNumber');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Konnte gespeichertes Profil nicht entfernen:', error);
|
||||||
|
}
|
||||||
|
pendingOpenCooldownMap = loadPendingOpenCooldownMap(null);
|
||||||
|
renderPosts();
|
||||||
|
applyAutoRefreshSettings();
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProfileSettings(settings, { rerender = true, refreshPosts = false } = {}) {
|
||||||
|
const nextActiveProfiles = normalizeActiveProfileNumbers(settings && settings.active_profiles);
|
||||||
|
activeProfileNumbers = nextActiveProfiles;
|
||||||
|
renderProfileSelectorOptions();
|
||||||
|
renderManualTargetOptions(manualPostTargetSelect ? manualPostTargetSelect.value : 1);
|
||||||
|
|
||||||
|
if (currentProfile !== null && !isSelectableProfileNumber(currentProfile)) {
|
||||||
|
clearSelectedProfile('Das gespeicherte Profil ist deaktiviert. Bitte wähle ein aktives Profil.');
|
||||||
|
} else if (profileSelectElement && isSelectableProfileNumber(currentProfile)) {
|
||||||
|
profileSelectElement.value = String(currentProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rerender && currentProfile !== null) {
|
||||||
|
renderPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshPosts) {
|
||||||
|
fetchPosts({ showLoader: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProfileSettings() {
|
||||||
|
try {
|
||||||
|
const response = await apiFetch(`${API_URL}/profile-settings`);
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
applyProfileSettings(data, { rerender: false });
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Profil-Einstellungen konnten nicht geladen werden:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchProfileState() {
|
async function fetchProfileState() {
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_URL}/profile-state`);
|
const response = await apiFetch(`${API_URL}/profile-state`);
|
||||||
@@ -3815,16 +3932,16 @@ async function pushProfileState(profileNumber) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureProfileSelected() {
|
function ensureProfileSelected() {
|
||||||
if (isValidProfileNumber(currentProfile)) {
|
if (isSelectableProfileNumber(currentProfile)) {
|
||||||
hideError();
|
hideError();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
showError('Bitte zuerst ein Profil auswählen.');
|
showError('Bitte zuerst ein aktives Profil auswählen.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
|
function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
|
||||||
if (!isValidProfileNumber(profileNumber)) {
|
if (!isSelectableProfileNumber(profileNumber)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3863,20 +3980,16 @@ function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
|
|||||||
// Load profile from localStorage
|
// Load profile from localStorage
|
||||||
function loadProfile() {
|
function loadProfile() {
|
||||||
fetchProfileState().then((backendProfile) => {
|
fetchProfileState().then((backendProfile) => {
|
||||||
if (isValidProfileNumber(backendProfile)) {
|
if (isSelectableProfileNumber(backendProfile)) {
|
||||||
applyProfileNumber(backendProfile, { fromBackend: true });
|
applyProfileNumber(backendProfile, { fromBackend: true });
|
||||||
} else {
|
} else {
|
||||||
const saved = localStorage.getItem('profileNumber');
|
const saved = localStorage.getItem('profileNumber');
|
||||||
const parsed = saved ? parseInt(saved, 10) : NaN;
|
const parsed = saved ? parseInt(saved, 10) : NaN;
|
||||||
if (isValidProfileNumber(parsed)) {
|
if (isSelectableProfileNumber(parsed)) {
|
||||||
applyProfileNumber(parsed, { fromBackend: true });
|
applyProfileNumber(parsed, { fromBackend: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (profileSelectElement) {
|
clearSelectedProfile('Bitte zuerst ein aktives Profil auswählen.');
|
||||||
profileSelectElement.value = '';
|
|
||||||
}
|
|
||||||
currentProfile = null;
|
|
||||||
showError('Bitte zuerst ein Profil auswählen.');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3893,8 +4006,12 @@ function startProfilePolling() {
|
|||||||
|
|
||||||
profilePollTimer = setInterval(async () => {
|
profilePollTimer = setInterval(async () => {
|
||||||
const backendProfile = await fetchProfileState();
|
const backendProfile = await fetchProfileState();
|
||||||
if (backendProfile && backendProfile !== currentProfile) {
|
if (isSelectableProfileNumber(backendProfile) && backendProfile !== currentProfile) {
|
||||||
applyProfileNumber(backendProfile, { fromBackend: true });
|
applyProfileNumber(backendProfile, { fromBackend: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (backendProfile === null && currentProfile !== null && !isSelectableProfileNumber(currentProfile)) {
|
||||||
|
clearSelectedProfile('Das gespeicherte Profil ist deaktiviert. Bitte wähle ein aktives Profil.');
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@@ -3903,8 +4020,8 @@ function startProfilePolling() {
|
|||||||
if (profileSelectElement) {
|
if (profileSelectElement) {
|
||||||
profileSelectElement.addEventListener('change', (e) => {
|
profileSelectElement.addEventListener('change', (e) => {
|
||||||
const parsed = parseInt(e.target.value, 10);
|
const parsed = parseInt(e.target.value, 10);
|
||||||
if (!isValidProfileNumber(parsed)) {
|
if (!isSelectableProfileNumber(parsed)) {
|
||||||
showError('Bitte zuerst ein Profil auswählen.');
|
showError('Bitte zuerst ein aktives Profil auswählen.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saveProfile(parsed);
|
saveProfile(parsed);
|
||||||
@@ -4984,7 +5101,7 @@ function createPostCard(post, status, meta = {}) {
|
|||||||
<div class="post-target">
|
<div class="post-target">
|
||||||
<span>Benötigte Profile:</span>
|
<span>Benötigte Profile:</span>
|
||||||
<select class="control-select post-target__select" data-post-id="${post.id}">
|
<select class="control-select post-target__select" data-post-id="${post.id}">
|
||||||
${Array.from({ length: MAX_PROFILES }, (_, index) => index + 1)
|
${Array.from({ length: getActiveTargetCountLimit() }, (_, index) => index + 1)
|
||||||
.map((value) => `
|
.map((value) => `
|
||||||
<option value="${value}" ${value === status.targetCount ? 'selected' : ''}>${value}</option>
|
<option value="${value}" ${value === status.targetCount ? 'selected' : ''}>${value}</option>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
@@ -5180,7 +5297,7 @@ function populateManualPostForm(post) {
|
|||||||
|
|
||||||
if (manualPostTargetSelect) {
|
if (manualPostTargetSelect) {
|
||||||
const targetValue = parseInt(post.target_count, 10);
|
const targetValue = parseInt(post.target_count, 10);
|
||||||
manualPostTargetSelect.value = Number.isNaN(targetValue) ? '1' : String(targetValue);
|
renderManualTargetOptions(Number.isNaN(targetValue) ? 1 : targetValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manualPostCreatorInput) {
|
if (manualPostCreatorInput) {
|
||||||
@@ -5206,7 +5323,7 @@ function resetManualPostForm({ keepMessages = false } = {}) {
|
|||||||
manualPostForm.reset();
|
manualPostForm.reset();
|
||||||
|
|
||||||
if (manualPostTargetSelect) {
|
if (manualPostTargetSelect) {
|
||||||
manualPostTargetSelect.value = '1';
|
renderManualTargetOptions(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manualPostUrlInput) {
|
if (manualPostUrlInput) {
|
||||||
@@ -5271,7 +5388,7 @@ async function updateTargetInline(postId, value, selectElement) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number.isNaN(value) || value < 1 || value > MAX_PROFILES) {
|
if (Number.isNaN(value) || value < 1 || value > getActiveTargetCountLimit()) {
|
||||||
renderPosts();
|
renderPosts();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5327,8 +5444,8 @@ async function handleManualPostSubmit(event) {
|
|||||||
|
|
||||||
const targetValue = manualPostTargetSelect ? manualPostTargetSelect.value : '1';
|
const targetValue = manualPostTargetSelect ? manualPostTargetSelect.value : '1';
|
||||||
const parsedTarget = parseInt(targetValue, 10);
|
const parsedTarget = parseInt(targetValue, 10);
|
||||||
if (Number.isNaN(parsedTarget) || parsedTarget < 1 || parsedTarget > MAX_PROFILES) {
|
if (Number.isNaN(parsedTarget) || parsedTarget < 1 || parsedTarget > getActiveTargetCountLimit()) {
|
||||||
displayManualPostMessage('Die Anzahl der benötigten Profile muss zwischen 1 und 5 liegen.', 'error');
|
displayManualPostMessage(getTargetCountErrorMessage(), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5603,6 +5720,11 @@ window.addEventListener('app:view-change', (event) => {
|
|||||||
updatePostsScrollTopButtonVisibility();
|
updatePostsScrollTopButtonVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('profile-settings-updated', (event) => {
|
||||||
|
const settings = event && event.detail ? event.detail : null;
|
||||||
|
applyProfileSettings(settings || null, { refreshPosts: true });
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'visible' && pendingAutoOpenEnabled) {
|
if (document.visibilityState === 'visible' && pendingAutoOpenEnabled) {
|
||||||
if (pendingAutoOpenTimerId) {
|
if (pendingAutoOpenTimerId) {
|
||||||
@@ -5629,6 +5751,9 @@ async function bootstrapApp() {
|
|||||||
initializeTabFromUrl();
|
initializeTabFromUrl();
|
||||||
updateMergeControlsUI();
|
updateMergeControlsUI();
|
||||||
loadSortMode();
|
loadSortMode();
|
||||||
|
renderProfileSelectorOptions();
|
||||||
|
renderManualTargetOptions(1);
|
||||||
|
await loadProfileSettings();
|
||||||
resetManualPostForm();
|
resetManualPostForm();
|
||||||
loadProfile();
|
loadProfile();
|
||||||
startProfilePolling();
|
startProfilePolling();
|
||||||
|
|||||||
@@ -1102,6 +1102,16 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Profile Friends Section -->
|
<!-- Profile Friends Section -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2 class="section-title">🧩 Aktive Profile</h2>
|
||||||
|
<p class="section-description">
|
||||||
|
Deaktivierte Profile werden in der Bestätigungsreihenfolge übersprungen. Die maximale Anzahl benötigter Profile reduziert sich automatisch auf die Zahl der aktiven Profile.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="profileActivationList" class="profile-activation-list"></div>
|
||||||
|
<p id="profileActivationSummary" class="form-help"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2 class="section-title">👥 Freundesnamen pro Profil</h2>
|
<h2 class="section-title">👥 Freundesnamen pro Profil</h2>
|
||||||
<p class="section-description">
|
<p class="section-description">
|
||||||
|
|||||||
@@ -95,6 +95,39 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-activation-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-activation-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #e4e6eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-activation-item__copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-activation-item__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1c1e21;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-activation-item__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #65676b;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-weights {
|
.grid-weights {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
|||||||
176
web/settings.js
176
web/settings.js
@@ -52,10 +52,13 @@ const AI_AUTO_COMMENT_RATE_LIMIT_DEFAULTS = Object.freeze({
|
|||||||
active_hours_start: '',
|
active_hours_start: '',
|
||||||
active_hours_end: ''
|
active_hours_end: ''
|
||||||
});
|
});
|
||||||
|
const MAX_PROFILES = 5;
|
||||||
|
const DEFAULT_ACTIVE_PROFILES = Array.from({ length: MAX_PROFILES }, (_unused, index) => index + 1);
|
||||||
|
|
||||||
let credentials = [];
|
let credentials = [];
|
||||||
let currentSettings = null;
|
let currentSettings = null;
|
||||||
let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 };
|
let hiddenSettings = { auto_purge_enabled: true, retention_days: 90 };
|
||||||
|
let profileSettings = { active_profiles: [...DEFAULT_ACTIVE_PROFILES], max_target_count: MAX_PROFILES };
|
||||||
const SPORT_WEIGHT_FIELDS = [
|
const SPORT_WEIGHT_FIELDS = [
|
||||||
{ key: 'scoreline', id: 'sportWeightScoreline' },
|
{ key: 'scoreline', id: 'sportWeightScoreline' },
|
||||||
{ key: 'scoreEmoji', id: 'sportWeightScoreEmoji' },
|
{ key: 'scoreEmoji', id: 'sportWeightScoreEmoji' },
|
||||||
@@ -1229,7 +1232,7 @@ async function testComment() {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(lastTest);
|
const data = JSON.parse(lastTest);
|
||||||
document.getElementById('testPostText').value = data.postText || '';
|
document.getElementById('testPostText').value = data.postText || '';
|
||||||
document.getElementById('testProfileNumber').value = data.profileNumber || '1';
|
renderTestProfileOptions(data.profileNumber || profileSettings.active_profiles[0] || 1);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load last test comment:', e);
|
console.error('Failed to load last test comment:', e);
|
||||||
}
|
}
|
||||||
@@ -1313,6 +1316,7 @@ async function saveAllSettings(event) {
|
|||||||
|
|
||||||
const results = await Promise.all([
|
const results = await Promise.all([
|
||||||
saveSettings(null, { silent: true }),
|
saveSettings(null, { silent: true }),
|
||||||
|
saveProfileSettings(null, { silent: true }),
|
||||||
saveHiddenSettings(null, { silent: true }),
|
saveHiddenSettings(null, { silent: true }),
|
||||||
saveModerationSettings(null, { silent: true }),
|
saveModerationSettings(null, { silent: true }),
|
||||||
saveSimilaritySettings(null, { silent: true }),
|
saveSimilaritySettings(null, { silent: true }),
|
||||||
@@ -1343,6 +1347,146 @@ async function saveAllSettings(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeActiveProfileNumbers(values) {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
return [...DEFAULT_ACTIVE_PROFILES];
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const normalized = values
|
||||||
|
.map((value) => parseInt(value, 10))
|
||||||
|
.filter((value) => Number.isInteger(value) && value >= 1 && value <= MAX_PROFILES && !seen.has(value) && seen.add(value))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
return normalized.length ? normalized : [...DEFAULT_ACTIVE_PROFILES];
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectActiveProfilesFromInputs() {
|
||||||
|
return Array.from(document.querySelectorAll('.profile-activation-checkbox:checked'))
|
||||||
|
.map((input) => parseInt(input.value, 10))
|
||||||
|
.filter((value) => Number.isInteger(value) && value >= 1 && value <= MAX_PROFILES)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTestProfileOptions(selectedValue = null) {
|
||||||
|
const select = document.getElementById('testProfileNumber');
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProfiles = normalizeActiveProfileNumbers(profileSettings.active_profiles);
|
||||||
|
const fallbackValue = activeProfiles[0] || 1;
|
||||||
|
const normalizedSelected = activeProfiles.includes(parseInt(selectedValue, 10))
|
||||||
|
? parseInt(selectedValue, 10)
|
||||||
|
: fallbackValue;
|
||||||
|
|
||||||
|
select.innerHTML = activeProfiles.map((profileNumber) => (
|
||||||
|
`<option value="${profileNumber}">Profil ${profileNumber}</option>`
|
||||||
|
)).join('');
|
||||||
|
select.value = String(normalizedSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProfileActivationSummary() {
|
||||||
|
const summary = document.getElementById('profileActivationSummary');
|
||||||
|
if (!summary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProfiles = collectActiveProfilesFromInputs();
|
||||||
|
summary.textContent = `Aktiv: ${activeProfiles.length} von ${MAX_PROFILES}. Max. benötigte Profile: ${Math.max(1, activeProfiles.length)}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProfileActivationList() {
|
||||||
|
const container = document.getElementById('profileActivationList');
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProfiles = normalizeActiveProfileNumbers(profileSettings.active_profiles);
|
||||||
|
container.innerHTML = DEFAULT_ACTIVE_PROFILES.map((profileNumber) => {
|
||||||
|
const checked = activeProfiles.includes(profileNumber);
|
||||||
|
return `
|
||||||
|
<label class="profile-activation-item">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-checkbox profile-activation-checkbox"
|
||||||
|
value="${profileNumber}"
|
||||||
|
${checked ? 'checked' : ''}
|
||||||
|
>
|
||||||
|
<span class="profile-activation-item__copy">
|
||||||
|
<span class="profile-activation-item__title">Profil ${profileNumber}</span>
|
||||||
|
<span class="profile-activation-item__hint">${checked ? 'Aktiv im Bestätigungsfluss' : 'Wird übersprungen'}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.querySelectorAll('.profile-activation-checkbox').forEach((checkbox) => {
|
||||||
|
checkbox.addEventListener('change', (event) => {
|
||||||
|
const checkedProfiles = collectActiveProfilesFromInputs();
|
||||||
|
if (!checkedProfiles.length) {
|
||||||
|
event.target.checked = true;
|
||||||
|
showError('❌ Mindestens ein Profil muss aktiv bleiben');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateProfileActivationSummary();
|
||||||
|
renderTestProfileOptions(document.getElementById('testProfileNumber')?.value || checkedProfiles[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateProfileActivationSummary();
|
||||||
|
renderTestProfileOptions(document.getElementById('testProfileNumber')?.value || activeProfiles[0] || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProfileSettings() {
|
||||||
|
const res = await apiFetch(`${API_URL}/profile-settings`);
|
||||||
|
if (!res.ok) throw new Error('Failed to load profile settings');
|
||||||
|
profileSettings = await res.json();
|
||||||
|
profileSettings.active_profiles = normalizeActiveProfileNumbers(profileSettings.active_profiles);
|
||||||
|
renderProfileActivationList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProfileSettings(event, { silent = false } = {}) {
|
||||||
|
if (event && typeof event.preventDefault === 'function') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activeProfiles = collectActiveProfilesFromInputs();
|
||||||
|
if (!activeProfiles.length) {
|
||||||
|
throw new Error('Mindestens ein Profil muss aktiv bleiben');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await apiFetch(`${API_URL}/profile-settings`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ active_profiles: activeProfiles })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => null);
|
||||||
|
throw new Error((err && err.error) || 'Fehler beim Speichern');
|
||||||
|
}
|
||||||
|
|
||||||
|
profileSettings = await res.json();
|
||||||
|
profileSettings.active_profiles = normalizeActiveProfileNumbers(profileSettings.active_profiles);
|
||||||
|
renderProfileActivationList();
|
||||||
|
if (!silent) {
|
||||||
|
await loadProfileFriends();
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('profile-settings-updated', { detail: profileSettings }));
|
||||||
|
if (!silent) {
|
||||||
|
showSuccess('✅ Aktive Profile gespeichert');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
if (!silent) {
|
||||||
|
showError('❌ ' + err.message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// PROFILE FRIENDS
|
// PROFILE FRIENDS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1353,10 +1497,11 @@ async function loadProfileFriends() {
|
|||||||
const list = document.getElementById('profileFriendsList');
|
const list = document.getElementById('profileFriendsList');
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
|
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= MAX_PROFILES; i++) {
|
||||||
const res = await apiFetch(`${API_URL}/profile-friends/${i}`);
|
const res = await apiFetch(`${API_URL}/profile-friends/${i}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
profileFriends[i] = data.friend_names || '';
|
profileFriends[i] = data.friend_names || '';
|
||||||
|
const isActive = normalizeActiveProfileNumbers(profileSettings.active_profiles).includes(i);
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'form-group';
|
div.className = 'form-group';
|
||||||
@@ -1365,7 +1510,7 @@ async function loadProfileFriends() {
|
|||||||
<input type="text" id="friends${i}" class="form-input"
|
<input type="text" id="friends${i}" class="form-input"
|
||||||
placeholder="z.B. Anna, Max, Lisa"
|
placeholder="z.B. Anna, Max, Lisa"
|
||||||
value="${escapeHtml(profileFriends[i])}">
|
value="${escapeHtml(profileFriends[i])}">
|
||||||
<p class="form-help">Kommagetrennte Liste von Freundesnamen für Profil ${i}</p>
|
<p class="form-help">Kommagetrennte Liste von Freundesnamen für Profil ${i}${isActive ? '' : ' (derzeit deaktiviert)'}</p>
|
||||||
`;
|
`;
|
||||||
list.appendChild(div);
|
list.appendChild(div);
|
||||||
|
|
||||||
@@ -1406,7 +1551,7 @@ async function saveFriends(profileNumber, friendNames, { silent = false } = {})
|
|||||||
|
|
||||||
async function saveAllFriends({ silent = false } = {}) {
|
async function saveAllFriends({ silent = false } = {}) {
|
||||||
let success = true;
|
let success = true;
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= MAX_PROFILES; i++) {
|
||||||
const input = document.getElementById(`friends${i}`);
|
const input = document.getElementById(`friends${i}`);
|
||||||
if (!input) {
|
if (!input) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1533,12 +1678,19 @@ if (similarityForm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
Promise.all([
|
(async () => {
|
||||||
loadCredentials(),
|
try {
|
||||||
loadSettings(),
|
await Promise.all([
|
||||||
loadHiddenSettings(),
|
loadCredentials(),
|
||||||
loadModerationSettings(),
|
loadSettings(),
|
||||||
loadSimilaritySettings(),
|
loadProfileSettings(),
|
||||||
loadProfileFriends()
|
loadHiddenSettings(),
|
||||||
]).catch(err => showError(err.message));
|
loadModerationSettings(),
|
||||||
|
loadSimilaritySettings()
|
||||||
|
]);
|
||||||
|
await loadProfileFriends();
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message);
|
||||||
|
}
|
||||||
|
})();
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user