const API_URL = 'https://fb.srv.medeba-media.de/api';
let initialViewParam = null;
try {
const params = new URLSearchParams(window.location.search);
initialViewParam = params.get('view');
} catch (error) {
console.warn('Konnte view-Parameter nicht auslesen:', error);
}
let focusPostIdParam = null;
let focusPostUrlParam = null;
let focusNormalizedUrl = '';
let focusHandled = false;
let initialTabOverride = null;
let focusTabAdjusted = null;
let currentProfile = 1;
let currentTab = 'pending';
let posts = [];
let includeExpiredPosts = false;
let profilePollTimer = null;
const UPDATES_RECONNECT_DELAY = 5000;
let updatesEventSource = null;
let updatesReconnectTimer = null;
let updatesStreamHealthy = false;
let updatesShouldResyncOnConnect = false;
const MAX_PROFILES = 5;
const PROFILE_NAMES = {
1: 'Profil 1',
2: 'Profil 2',
3: 'Profil 3',
4: 'Profil 4',
5: 'Profil 5'
};
function apiFetch(url, options = {}) {
const config = {
...options,
credentials: 'include'
};
if (options && options.headers) {
config.headers = { ...options.headers };
}
return fetch(url, config);
}
function sortPostsByCreatedAt() {
posts.sort((a, b) => toTimestamp(b.created_at, 0) - toTimestamp(a.created_at, 0));
}
function applyPostUpdateFromStream(post) {
if (!post || !post.id) {
return;
}
const index = posts.findIndex((item) => item.id === post.id);
if (index !== -1) {
posts[index] = post;
} else {
posts.push(post);
}
sortPostsByCreatedAt();
if (manualPostMode === 'edit' && manualPostEditingId === post.id) {
populateManualPostForm(post);
}
renderPosts();
}
function removePostFromCache(postId) {
if (!postId) {
return;
}
const index = posts.findIndex((item) => item.id === postId);
if (index === -1) {
return;
}
posts.splice(index, 1);
if (
manualPostMode === 'edit'
&& manualPostEditingId === postId
&& manualPostModal
&& manualPostModal.classList.contains('open')
) {
closeManualPostModal();
}
renderPosts();
}
function handleBackendEvent(eventPayload) {
if (!eventPayload || typeof eventPayload !== 'object') {
return;
}
switch (eventPayload.type) {
case 'post-upsert':
if (eventPayload.post) {
applyPostUpdateFromStream(eventPayload.post);
}
break;
case 'post-deleted':
if (eventPayload.postId) {
removePostFromCache(eventPayload.postId);
}
break;
case 'connected':
case 'heartbeat':
default:
break;
}
}
function scheduleUpdatesReconnect() {
if (updatesReconnectTimer) {
return;
}
updatesReconnectTimer = setTimeout(() => {
updatesReconnectTimer = null;
startUpdatesStream();
}, UPDATES_RECONNECT_DELAY);
}
function startUpdatesStream() {
if (typeof EventSource === 'undefined') {
console.warn('EventSource wird von diesem Browser nicht unterstützt. Fallback auf Polling.');
return;
}
if (updatesEventSource) {
return;
}
const eventsUrl = `${API_URL}/events`;
let eventSource;
try {
eventSource = new EventSource(eventsUrl, { withCredentials: true });
} catch (error) {
console.warn('Konnte Update-Stream nicht starten:', error);
scheduleUpdatesReconnect();
return;
}
updatesEventSource = eventSource;
eventSource.addEventListener('open', () => {
updatesStreamHealthy = true;
if (updatesReconnectTimer) {
clearTimeout(updatesReconnectTimer);
updatesReconnectTimer = null;
}
if (updatesShouldResyncOnConnect) {
updatesShouldResyncOnConnect = false;
fetchPosts({ showLoader: false });
}
applyAutoRefreshSettings();
});
eventSource.addEventListener('message', (event) => {
if (!event || typeof event.data !== 'string' || !event.data.trim()) {
return;
}
let payload;
try {
payload = JSON.parse(event.data);
} catch (error) {
console.warn('Ungültige Daten vom Update-Stream erhalten:', error);
return;
}
handleBackendEvent(payload);
});
eventSource.addEventListener('error', () => {
if (updatesEventSource) {
updatesEventSource.close();
updatesEventSource = null;
}
if (!updatesShouldResyncOnConnect) {
updatesShouldResyncOnConnect = true;
}
updatesStreamHealthy = false;
applyAutoRefreshSettings();
fetchPosts({ showLoader: false });
scheduleUpdatesReconnect();
});
}
const screenshotModal = document.getElementById('screenshotModal');
const screenshotModalContent = document.getElementById('screenshotModalContent');
const screenshotModalImage = document.getElementById('screenshotModalImage');
const screenshotModalClose = document.getElementById('screenshotModalClose');
const screenshotModalBackdrop = document.getElementById('screenshotModalBackdrop');
let screenshotModalLastFocus = null;
let screenshotModalPreviousOverflow = '';
let screenshotModalZoomed = false;
const manualPostForm = document.getElementById('manualPostForm');
const manualPostUrlInput = document.getElementById('manualPostUrl');
const manualPostTitleInput = document.getElementById('manualPostTitle');
const manualPostTargetSelect = document.getElementById('manualPostTarget');
const manualPostCreatorInput = document.getElementById('manualPostCreatorName');
const manualPostDeadlineInput = document.getElementById('manualPostDeadline');
const manualPostResetButton = document.getElementById('manualPostReset');
const manualPostMessage = document.getElementById('manualPostMessage');
const manualPostSubmitButton = document.getElementById('manualPostSubmitBtn');
const manualPostModal = document.getElementById('manualPostModal');
const manualPostModalContent = document.getElementById('manualPostModalContent');
const manualPostModalBackdrop = document.getElementById('manualPostModalBackdrop');
const manualPostModalClose = document.getElementById('manualPostModalClose');
const manualPostModalTitle = document.getElementById('manualPostModalTitle');
const openManualPostModalBtn = document.getElementById('openManualPostModalBtn');
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
const autoRefreshIntervalSelect = document.getElementById('autoRefreshInterval');
const sortModeSelect = document.getElementById('sortMode');
const sortDirectionToggle = document.getElementById('sortDirectionToggle');
const bookmarkPanelToggle = document.getElementById('bookmarkPanelToggle');
const bookmarkPanel = document.getElementById('bookmarkPanel');
const bookmarkPanelClose = document.getElementById('bookmarkPanelClose');
const bookmarksList = document.getElementById('bookmarksList');
const bookmarkForm = document.getElementById('bookmarkForm');
const bookmarkNameInput = document.getElementById('bookmarkName');
const bookmarkQueryInput = document.getElementById('bookmarkQuery');
const bookmarkCancelBtn = document.getElementById('bookmarkCancelBtn');
const bookmarkSearchInput = document.getElementById('bookmarkSearchInput');
const bookmarkSortSelect = document.getElementById('bookmarkSortSelect');
const bookmarkSortDirectionToggle = document.getElementById('bookmarkSortDirectionToggle');
const profileSelectElement = document.getElementById('profileSelect');
const includeExpiredToggle = document.getElementById('includeExpiredToggle');
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
const SORT_SETTINGS_KEY = 'trackerSortSettings';
const SORT_SETTINGS_LEGACY_KEY = 'trackerSortMode';
const FACEBOOK_TRACKING_PARAMS = ['__cft__', '__tn__', '__eep__', 'mibextid', 'set', 'comment_id', 'hoisted_section_header_type'];
const VALID_SORT_MODES = new Set(['created', 'deadline', 'smart', 'lastCheck', 'lastChange']);
const DEFAULT_SORT_SETTINGS = { mode: 'created', direction: 'desc' };
const BOOKMARKS_BASE_URL = 'https://www.facebook.com/search/top';
const BOOKMARK_WINDOW_DAYS = 28;
const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences';
const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired';
function loadIncludeExpiredPreference() {
try {
const stored = localStorage.getItem(INCLUDE_EXPIRED_STORAGE_KEY);
if (stored === 'true') {
return true;
}
if (stored === 'false') {
return false;
}
} catch (error) {
console.warn('Konnte Anzeigepräferenz für abgelaufene Beiträge nicht laden:', error);
}
return false;
}
function persistIncludeExpiredPreference(value) {
try {
localStorage.setItem(INCLUDE_EXPIRED_STORAGE_KEY, value ? 'true' : 'false');
} catch (error) {
console.warn('Konnte Anzeigepräferenz für abgelaufene Beiträge nicht speichern:', error);
}
}
function updateIncludeExpiredToggleUI() {
if (!includeExpiredToggle) {
return;
}
includeExpiredToggle.checked = includeExpiredPosts;
}
includeExpiredPosts = loadIncludeExpiredPreference();
function updateIncludeExpiredToggleVisibility() {
if (!includeExpiredToggle) {
return;
}
const wrapper = includeExpiredToggle.closest('.search-filter-toggle');
if (!wrapper) {
return;
}
wrapper.style.display = currentTab === 'all' ? 'inline-flex' : 'none';
}
function initializeFocusParams() {
try {
const params = new URLSearchParams(window.location.search);
const postIdParam = params.get('postId');
const postUrlParam = params.get('postUrl');
if (postIdParam && postIdParam.trim()) {
focusPostIdParam = postIdParam.trim();
initialTabOverride = initialTabOverride || 'all';
}
if (postUrlParam && postUrlParam.trim()) {
focusPostUrlParam = postUrlParam.trim();
const normalized = normalizeFacebookPostUrl(focusPostUrlParam);
focusNormalizedUrl = normalized || focusPostUrlParam;
initialTabOverride = initialTabOverride || 'all';
}
focusHandled = false;
focusTabAdjusted = null;
} catch (error) {
console.warn('Konnte Fokus-Parameter nicht verarbeiten:', error);
}
}
let autoRefreshTimer = null;
let autoRefreshSettings = {
enabled: false,
interval: 30000
};
let sortMode = DEFAULT_SORT_SETTINGS.mode;
let sortDirection = DEFAULT_SORT_SETTINGS.direction;
let isFetchingPosts = false;
let manualPostMode = 'create';
let manualPostEditingId = null;
let manualPostModalLastFocus = null;
let manualPostModalPreviousOverflow = '';
let activeDeadlinePicker = null;
let bookmarkPanelVisible = false;
let bookmarkOutsideHandler = null;
let bookmarkSearchTerm = '';
let bookmarkSortMode = 'recent';
let bookmarkSortDirection = 'desc';
function loadBookmarkPreferences() {
try {
const stored = localStorage.getItem(BOOKMARK_PREFS_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed && typeof parsed === 'object') {
return parsed;
}
}
} catch (error) {
console.warn('Konnte Bookmark-Einstellungen nicht laden:', error);
}
return {};
}
function persistBookmarkPreferences() {
try {
const payload = {
searchTerm: bookmarkSearchTerm,
sortMode: bookmarkSortMode,
sortDirection: bookmarkSortDirection
};
localStorage.setItem(BOOKMARK_PREFS_KEY, JSON.stringify(payload));
} catch (error) {
console.warn('Konnte Bookmark-Einstellungen nicht speichern:', error);
}
}
const storedBookmarkPrefs = loadBookmarkPreferences();
if (storedBookmarkPrefs.searchTerm && typeof storedBookmarkPrefs.searchTerm === 'string') {
bookmarkSearchTerm = storedBookmarkPrefs.searchTerm.trim();
}
if (storedBookmarkPrefs.sortMode === 'label' || storedBookmarkPrefs.sortMode === 'recent') {
bookmarkSortMode = storedBookmarkPrefs.sortMode;
}
if (storedBookmarkPrefs.sortDirection === 'asc' || storedBookmarkPrefs.sortDirection === 'desc') {
bookmarkSortDirection = storedBookmarkPrefs.sortDirection;
}
const INITIAL_POST_LIMIT = 10;
const POST_LOAD_INCREMENT = 10;
const tabVisibleCounts = {
pending: INITIAL_POST_LIMIT,
all: INITIAL_POST_LIMIT
};
const tabFilteredCounts = {
pending: 0,
all: 0
};
let loadMoreObserver = null;
let observedLoadMoreElement = null;
function getProfileName(profileNumber) {
if (!profileNumber) {
return '';
}
return PROFILE_NAMES[profileNumber] || `Profil ${profileNumber}`;
}
function formatDateTime(value) {
if (!value) {
return '';
}
try {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
return date.toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
} catch (error) {
console.warn('Ungültiges Datum:', error);
return '';
}
}
function formatDeadline(value) {
if (!value) {
return 'Keine Deadline';
}
try {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return 'Ungültige Deadline';
}
return date.toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
} catch (error) {
console.warn('Ungültige Deadline:', error);
return 'Ungültige Deadline';
}
}
function formatUrlForDisplay(url) {
if (!url) {
return '';
}
try {
const parsed = new URL(url);
const pathname = parsed.pathname === '/' ? '' : parsed.pathname;
const search = parsed.search || '';
return `${parsed.hostname}${pathname}${search}`;
} catch (error) {
return url;
}
}
function toTimestamp(value, fallback = null) {
if (!value) {
return fallback;
}
const timestamp = value instanceof Date ? value.getTime() : new Date(value).getTime();
if (Number.isNaN(timestamp)) {
return fallback;
}
return timestamp;
}
function getSortTabKey(tab = currentTab) {
if (tab === 'all') {
return 'all';
}
return 'pending';
}
function normalizeSortMode(value) {
if (VALID_SORT_MODES.has(value)) {
return value;
}
return DEFAULT_SORT_SETTINGS.mode;
}
function normalizeSortDirection(value) {
return value === 'asc' ? 'asc' : DEFAULT_SORT_SETTINGS.direction;
}
function getSortStorage() {
try {
const raw = localStorage.getItem(SORT_SETTINGS_KEY) || localStorage.getItem(SORT_SETTINGS_LEGACY_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed;
}
} catch (error) {
console.warn('Konnte Sortierdaten nicht parsen:', error);
}
return {};
}
function persistSortStorage(storage) {
try {
localStorage.setItem(SORT_SETTINGS_KEY, JSON.stringify(storage));
if (SORT_SETTINGS_KEY !== SORT_SETTINGS_LEGACY_KEY) {
localStorage.removeItem(SORT_SETTINGS_LEGACY_KEY);
}
} catch (error) {
console.warn('Konnte Sortierdaten nicht speichern:', error);
}
}
function normalizeServerBookmark(entry) {
if (!entry || typeof entry !== 'object') {
return null;
}
const id = typeof entry.id === 'string' ? entry.id : null;
const query = typeof entry.query === 'string' ? entry.query.trim() : '';
if (!id || !query) {
return null;
}
const label = typeof entry.label === 'string' && entry.label.trim() ? entry.label.trim() : query;
const normalizeDate = (value) => {
if (!value) {
return null;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date.toISOString();
};
return {
id,
label,
query,
created_at: normalizeDate(entry.created_at),
updated_at: normalizeDate(entry.updated_at),
last_clicked_at: normalizeDate(entry.last_clicked_at),
deletable: true
};
}
function deduplicateBookmarks(list) {
const seen = new Set();
const deduped = [];
list.forEach((bookmark) => {
if (!bookmark || !bookmark.query) {
return;
}
const key = bookmark.query.toLowerCase();
if (seen.has(key)) {
return;
}
seen.add(key);
deduped.push(bookmark);
});
return deduped;
}
function sortBookmarksByRecency(list) {
return [...list].sort((a, b) => {
const aClick = a.last_clicked_at ? new Date(a.last_clicked_at).getTime() : -Infinity;
const bClick = b.last_clicked_at ? new Date(b.last_clicked_at).getTime() : -Infinity;
if (aClick !== bClick) {
return bClick - aClick;
}
const aCreated = a.created_at ? new Date(a.created_at).getTime() : -Infinity;
const bCreated = b.created_at ? new Date(b.created_at).getTime() : -Infinity;
if (aCreated !== bCreated) {
return bCreated - aCreated;
}
return a.label.localeCompare(b.label, 'de', { sensitivity: 'base' });
});
}
function getBookmarkLabelForComparison(bookmark = {}) {
const label = typeof bookmark.label === 'string' ? bookmark.label.trim() : '';
const query = typeof bookmark.query === 'string' ? bookmark.query.trim() : '';
return label || query || '';
}
function getBookmarkClickTimestamp(bookmark = {}) {
if (bookmark.last_clicked_at) {
const ts = new Date(bookmark.last_clicked_at).getTime();
if (!Number.isNaN(ts)) {
return ts;
}
}
return 0;
}
function sortBookmarksForDisplay(list) {
const items = [...list];
if (bookmarkSortMode === 'label') {
items.sort((a, b) => {
const labelA = getBookmarkLabelForComparison(a);
const labelB = getBookmarkLabelForComparison(b);
const result = labelA.localeCompare(labelB, 'de', { sensitivity: 'base' });
return bookmarkSortDirection === 'desc' ? -result : result;
});
return items;
}
items.sort((a, b) => {
const diff = getBookmarkClickTimestamp(b) - getBookmarkClickTimestamp(a);
if (diff !== 0) {
return bookmarkSortDirection === 'desc' ? diff : -diff;
}
const fallback = getBookmarkLabelForComparison(a).localeCompare(
getBookmarkLabelForComparison(b),
'de',
{ sensitivity: 'base' }
);
return bookmarkSortDirection === 'desc' ? fallback : -fallback;
});
return items;
}
function filterBookmarksBySearch(list) {
if (!bookmarkSearchTerm) {
return [...list];
}
const term = bookmarkSearchTerm.toLowerCase();
return list.filter((bookmark) => {
const label = (bookmark.label || bookmark.query || '').toLowerCase();
const query = (bookmark.query || '').toLowerCase();
return label.includes(term) || query.includes(term);
});
}
function updateBookmarkSortDirectionUI() {
if (!bookmarkSortDirectionToggle) {
return;
}
const isAsc = bookmarkSortDirection === 'asc';
bookmarkSortDirectionToggle.setAttribute('aria-pressed', isAsc ? 'true' : 'false');
bookmarkSortDirectionToggle.setAttribute('title', isAsc ? 'Älteste zuerst' : 'Neueste zuerst');
const icon = bookmarkSortDirectionToggle.querySelector('.bookmark-sort__direction-icon');
if (icon) {
icon.textContent = isAsc ? '▲' : '▼';
} else {
bookmarkSortDirectionToggle.textContent = isAsc ? '▲' : '▼';
}
}
const DEFAULT_BOOKMARK_LAST_CLICK_KEY = 'trackerDefaultBookmarkLastClickedAt';
const bookmarkState = {
items: [],
loaded: false,
loading: false,
error: null,
defaultLastClickedAt: null
};
try {
const storedDefaultBookmark = localStorage.getItem(DEFAULT_BOOKMARK_LAST_CLICK_KEY);
if (storedDefaultBookmark) {
bookmarkState.defaultLastClickedAt = storedDefaultBookmark;
}
} catch (error) {
console.warn('Konnte letzte Nutzung des Standard-Bookmarks nicht laden:', error);
}
let bookmarkFetchPromise = null;
function formatRelativeTimeFromNow(timestamp) {
if (!timestamp) {
return 'Noch nie geöffnet';
}
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) {
return 'Zuletzt: unbekannt';
}
const diffMs = Date.now() - date.getTime();
if (diffMs < 0) {
return 'gerade eben';
}
const diffSeconds = Math.floor(diffMs / 1000);
if (diffSeconds < 45) {
return 'vor wenigen Sekunden';
}
const diffMinutes = Math.floor(diffSeconds / 60);
if (diffMinutes < 60) {
return `vor ${diffMinutes} ${diffMinutes === 1 ? 'Minute' : 'Minuten'}`;
}
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) {
return `vor ${diffHours} ${diffHours === 1 ? 'Stunde' : 'Stunden'}`;
}
const diffDays = Math.floor(diffHours / 24);
if (diffDays < 31) {
return `vor ${diffDays} ${diffDays === 1 ? 'Tag' : 'Tagen'}`;
}
const diffMonths = Math.floor(diffDays / 30);
if (diffMonths < 12) {
return `vor ${diffMonths} ${diffMonths === 1 ? 'Monat' : 'Monaten'}`;
}
const diffYears = Math.floor(diffMonths / 12);
return `vor ${diffYears} ${diffYears === 1 ? 'Jahr' : 'Jahren'}`;
}
function upsertBookmarkInState(bookmark) {
const normalized = normalizeServerBookmark(bookmark);
if (!normalized) {
return;
}
const lowerQuery = normalized.query.toLowerCase();
const existingIndex = bookmarkState.items.findIndex((item) => {
if (!item || !item.query) {
return false;
}
return item.id === normalized.id || item.query.toLowerCase() === lowerQuery;
});
if (existingIndex >= 0) {
bookmarkState.items[existingIndex] = { ...bookmarkState.items[existingIndex], ...normalized };
} else {
bookmarkState.items.push(normalized);
}
bookmarkState.items = deduplicateBookmarks(sortBookmarksByRecency(bookmarkState.items));
}
function removeBookmarkFromState(bookmarkId) {
if (!bookmarkId) {
return;
}
bookmarkState.items = bookmarkState.items.filter((bookmark) => bookmark.id !== bookmarkId);
}
async function refreshBookmarks(options = {}) {
const { force = false } = options;
if (bookmarkFetchPromise && !force) {
return bookmarkFetchPromise;
}
bookmarkFetchPromise = (async () => {
bookmarkState.loading = true;
if (!bookmarkState.loaded || force) {
renderBookmarks();
}
try {
const response = await apiFetch(`${API_URL}/bookmarks`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const normalized = Array.isArray(data)
? data.map(normalizeServerBookmark).filter(Boolean)
: [];
const finalList = deduplicateBookmarks(sortBookmarksByRecency(normalized));
bookmarkState.items = finalList;
bookmarkState.loaded = true;
bookmarkState.loading = false;
bookmarkState.error = null;
renderBookmarks();
return bookmarkState.items;
} catch (error) {
console.warn('Konnte Bookmarks nicht laden:', error);
bookmarkState.error = 'Bookmarks konnten nicht geladen werden.';
bookmarkState.loading = false;
if (!bookmarkState.loaded) {
bookmarkState.items = [];
}
renderBookmarks();
throw error;
} finally {
bookmarkFetchPromise = null;
}
})();
return bookmarkFetchPromise;
}
function formatFacebookDateParts(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const monthLabel = `${year}-${month}`;
const dayLabel = `${year}-${month}-${day}`;
return {
year: String(year),
monthLabel,
dayLabel
};
}
function buildBookmarkFiltersParam() {
const y = String(new Date().getFullYear()); // "2025"
// start_month = Monat von (heute - BOOKMARK_WINDOW_DAYS), auf Monatsanfang (ohne Padding)
const windowAgo = new Date();
windowAgo.setDate(windowAgo.getDate() - BOOKMARK_WINDOW_DAYS);
const startMonthNum = windowAgo.getMonth() + 1; // 1..12
const startMonthLabel = `${y}-${startMonthNum}`; // z.B. "2025-9"
const startDayLabel = `${startMonthLabel}-1`; // z.B. "2025-9-1"
// Ende = Jahresende (ohne Padding), Jahre immer aktuelles Jahr als String
const endMonthLabel = `${y}-12`;
const endDayLabel = `${y}-12-31`;
// Reihenfolge wie gewünscht: top_tab zuerst, dann rp_creation_time
const filtersPayload = {
'top_tab_recent_posts:0': JSON.stringify({
name: 'top_tab_recent_posts',
args: ''
}),
'rp_creation_time:0': JSON.stringify({
name: 'creation_time',
args: JSON.stringify({
start_year: y, // als String
start_month: startMonthLabel,
end_year: y, // als String
end_month: endMonthLabel,
start_day: startDayLabel,
end_day: endDayLabel
})
})
};
const serialized = JSON.stringify(filtersPayload);
// Rohes Base64 zurückgeben (kein encodeURIComponent!)
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
return window.btoa(serialized);
} else if (typeof btoa === 'function') {
return btoa(serialized);
} else if (typeof Buffer !== 'undefined') {
return Buffer.from(serialized, 'utf8').toString('base64');
} else {
console.warn('Base64 nicht verfügbar, gebe JSON zurück (wird von URLSearchParams encodet).');
return serialized;
}
}
/*
function buildBookmarkFiltersParam() {
const y = String(new Date().getFullYear()); // "2025"
// WICHTIG: Schlüssel-Reihenfolge wie im 'soll' → top_tab zuerst
const filtersPayload = {
'top_tab_recent_posts:0': JSON.stringify({
name: 'top_tab_recent_posts',
args: ''
}),
'rp_creation_time:0': JSON.stringify({
name: 'creation_time',
args: JSON.stringify({
start_year: y, // als String
start_month: `${y}-1`, // ohne Padding
end_year: y, // als String
end_month: `${y}-12`,
start_day: `${y}-1-1`,
end_day: `${y}-12-31`
})
})
};
const serialized = JSON.stringify(filtersPayload);
// Base64 OHNE URL-Encode zurückgeben
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
return window.btoa(serialized);
} else if (typeof btoa === 'function') {
return btoa(serialized);
} else if (typeof Buffer !== 'undefined') {
return Buffer.from(serialized, 'utf8').toString('base64');
} else {
console.warn('Base64 nicht verfügbar, gebe JSON zurück (wird von URLSearchParams encodet).');
return serialized; // kein encodeURIComponent!
}
}
*/
function buildBookmarkSearchUrl(query) {
const trimmed = typeof query === 'string' ? query.trim() : '';
if (!trimmed) {
return null;
}
const searchUrl = new URL(BOOKMARKS_BASE_URL);
searchUrl.searchParams.set('q', trimmed);
searchUrl.searchParams.set('filters', buildBookmarkFiltersParam());
return searchUrl.toString();
}
function buildBookmarkSearchQueries(baseQuery) {
const trimmed = typeof baseQuery === 'string' ? baseQuery.trim() : '';
if (!trimmed) {
return [...BOOKMARK_SUFFIXES];
}
return BOOKMARK_SUFFIXES.map((suffix) => `${trimmed} ${suffix}`.trim());
}
function openBookmark(bookmark) {
if (!bookmark) {
return;
}
const queries = buildBookmarkSearchQueries(bookmark.query);
if (!queries.length) {
queries.push('');
}
const stateBookmark = bookmarkState.items.find((item) => item.id === bookmark.id) || bookmark;
const isDefaultBookmark = stateBookmark && stateBookmark.isDefault;
const nowIso = new Date().toISOString();
if (isDefaultBookmark) {
bookmarkState.defaultLastClickedAt = nowIso;
try {
localStorage.setItem(DEFAULT_BOOKMARK_LAST_CLICK_KEY, nowIso);
} catch (error) {
console.warn('Konnte Standard-Bookmark-Zeit nicht speichern:', error);
}
renderBookmarks();
} else if (stateBookmark && stateBookmark.id && stateBookmark.deletable !== false) {
upsertBookmarkInState({
id: stateBookmark.id,
label: stateBookmark.label,
query: stateBookmark.query,
last_clicked_at: nowIso,
created_at: stateBookmark.created_at || nowIso,
updated_at: nowIso
});
renderBookmarks();
markBookmarkClick(stateBookmark.id);
}
queries.forEach((searchTerm) => {
const url = buildBookmarkSearchUrl(searchTerm);
if (url) {
window.open(url, '_blank', 'noopener');
}
});
}
async function markBookmarkClick(bookmarkId) {
if (!bookmarkId) {
return;
}
try {
const response = await apiFetch(`${API_URL}/bookmarks/${encodeURIComponent(bookmarkId)}/click`, {
method: 'POST'
});
if (!response.ok) {
return;
}
const updated = await response.json();
upsertBookmarkInState(updated);
renderBookmarks();
} catch (error) {
console.warn('Konnte Bookmark-Klick nicht speichern:', error);
}
}
async function removeBookmark(bookmarkId) {
if (!bookmarkId) {
return;
}
try {
const response = await apiFetch(`${API_URL}/bookmarks/${encodeURIComponent(bookmarkId)}`, {
method: 'DELETE'
});
if (response.ok || response.status === 204 || response.status === 404) {
removeBookmarkFromState(bookmarkId);
bookmarkState.error = null;
renderBookmarks();
return;
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
console.warn('Konnte Bookmark nicht löschen:', error);
bookmarkState.error = 'Bookmark konnte nicht gelöscht werden.';
renderBookmarks();
}
}
function createBookmarkRow(bookmark) {
const row = document.createElement('div');
row.className = 'bookmark-row';
row.dataset.query = bookmark.query || '';
if (!bookmark.last_clicked_at) {
row.dataset.state = 'never-used';
}
if (bookmark.isDefault) {
row.dataset.default = '1';
}
const openButton = document.createElement('button');
openButton.type = 'button';
openButton.className = 'bookmark-row__open';
const searchVariants = buildBookmarkSearchQueries(bookmark.query);
if (searchVariants.length) {
openButton.title = searchVariants.map((variant) => `• ${variant}`).join('\n');
}
openButton.addEventListener('click', () => openBookmark(bookmark));
const label = document.createElement('span');
label.className = 'bookmark-row__label';
label.textContent = bookmark.label || bookmark.query || 'Bookmark';
openButton.appendChild(label);
const query = document.createElement('span');
query.className = 'bookmark-row__query';
query.textContent = bookmark.query ? `„${bookmark.query}“` : 'Standard-Keywords';
openButton.appendChild(query);
row.appendChild(openButton);
const meta = document.createElement('span');
meta.className = 'bookmark-row__meta';
meta.textContent = formatRelativeTimeFromNow(bookmark.last_clicked_at);
if (bookmark.last_clicked_at) {
const date = new Date(bookmark.last_clicked_at);
if (!Number.isNaN(date.getTime())) {
meta.title = date.toLocaleString();
}
}
row.appendChild(meta);
if (bookmark.deletable !== false) {
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'bookmark-row__remove';
removeBtn.setAttribute('aria-label', `${bookmark.label || bookmark.query} entfernen`);
removeBtn.textContent = '×';
removeBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
removeBookmark(bookmark.id);
});
row.appendChild(removeBtn);
}
return row;
}
function renderBookmarks() {
if (!bookmarksList) {
return;
}
bookmarksList.innerHTML = '';
if (bookmarkState.loading && !bookmarkState.loaded) {
const loading = document.createElement('div');
loading.className = 'bookmark-status bookmark-status--loading';
loading.textContent = 'Lade Bookmarks...';
bookmarksList.appendChild(loading);
return;
}
if (bookmarkState.error && !bookmarkState.loaded) {
const errorNode = document.createElement('div');
errorNode.className = 'bookmark-status bookmark-status--error';
errorNode.textContent = bookmarkState.error;
bookmarksList.appendChild(errorNode);
return;
}
if (bookmarkState.error && bookmarkState.loaded) {
const warnNode = document.createElement('div');
warnNode.className = 'bookmark-status bookmark-status--error';
warnNode.textContent = bookmarkState.error;
bookmarksList.appendChild(warnNode);
}
const dynamicBookmarks = bookmarkState.items;
const staticDefault = {
id: 'default-search',
label: 'Gewinnspiel / gewinnen / verlosen',
query: '',
last_clicked_at: bookmarkState.defaultLastClickedAt || null,
deletable: false,
isDefault: true
};
const filteredBookmarks = filterBookmarksBySearch(dynamicBookmarks);
const sortedForAll = sortBookmarksForDisplay(filteredBookmarks);
const displayList = bookmarkSearchTerm ? sortedForAll : [staticDefault, ...sortedForAll];
const titleText = bookmarkSearchTerm
? (filteredBookmarks.length ? `Suchergebnisse (${filteredBookmarks.length})` : 'Keine Treffer')
: 'Alle Bookmarks';
if (!displayList.length) {
const empty = document.createElement('div');
empty.className = 'bookmark-empty';
if (bookmarkSearchTerm) {
empty.textContent = `Keine Bookmarks gefunden für „${bookmarkSearchTerm}“.`;
} else {
empty.textContent = 'Noch keine Bookmarks gespeichert.';
}
bookmarksList.appendChild(empty);
return;
}
const sectionElement = document.createElement('section');
sectionElement.className = 'bookmark-section';
sectionElement.dataset.section = bookmarkSearchTerm ? 'search' : 'all';
const headerEl = document.createElement('header');
headerEl.className = 'bookmark-section__header';
const titleEl = document.createElement('h3');
titleEl.className = 'bookmark-section__title';
titleEl.textContent = titleText;
headerEl.appendChild(titleEl);
sectionElement.appendChild(headerEl);
const list = document.createElement('div');
list.className = 'bookmark-section__list';
displayList.forEach((bookmark) => {
list.appendChild(createBookmarkRow(bookmark));
});
sectionElement.appendChild(list);
bookmarksList.appendChild(sectionElement);
}
function resetBookmarkForm() {
if (!bookmarkForm) {
return;
}
bookmarkForm.reset();
if (bookmarkNameInput) {
bookmarkNameInput.value = '';
}
if (bookmarkQueryInput) {
bookmarkQueryInput.value = '';
}
}
function ensureBookmarkOutsideHandler() {
if (bookmarkOutsideHandler) {
return bookmarkOutsideHandler;
}
bookmarkOutsideHandler = (event) => {
if (!bookmarkPanelVisible) {
return;
}
const target = event.target;
const insidePanel = bookmarkPanel && bookmarkPanel.contains(target);
const onToggle = bookmarkPanelToggle && bookmarkPanelToggle.contains(target);
if (!insidePanel && !onToggle) {
toggleBookmarkPanel(false);
}
};
return bookmarkOutsideHandler;
}
function removeBookmarkOutsideHandler() {
if (!bookmarkOutsideHandler) {
return;
}
document.removeEventListener('mousedown', bookmarkOutsideHandler, true);
document.removeEventListener('focusin', bookmarkOutsideHandler);
}
function toggleBookmarkPanel(forceVisible) {
if (!bookmarkPanel || !bookmarkPanelToggle) {
return;
}
const shouldShow = typeof forceVisible === 'boolean' ? forceVisible : !bookmarkPanelVisible;
if (shouldShow === bookmarkPanelVisible) {
return;
}
bookmarkPanelVisible = shouldShow;
bookmarkPanel.hidden = !bookmarkPanelVisible;
bookmarkPanel.style.display = bookmarkPanelVisible ? 'flex' : 'none';
bookmarkPanel.setAttribute('aria-hidden', bookmarkPanelVisible ? 'false' : 'true');
bookmarkPanelToggle.setAttribute('aria-expanded', bookmarkPanelVisible ? 'true' : 'false');
if (bookmarkPanelVisible) {
if (!bookmarkState.loaded && !bookmarkState.loading) {
bookmarkState.loading = true;
renderBookmarks();
refreshBookmarks().catch(() => {});
} else {
renderBookmarks();
}
resetBookmarkForm();
if (bookmarkQueryInput) {
window.requestAnimationFrame(() => {
bookmarkQueryInput.focus();
});
}
const handler = ensureBookmarkOutsideHandler();
window.requestAnimationFrame(() => {
document.addEventListener('mousedown', handler, true);
document.addEventListener('focusin', handler);
});
} else {
resetBookmarkForm();
removeBookmarkOutsideHandler();
bookmarkState.error = null;
if (bookmarkPanelToggle) {
bookmarkPanelToggle.focus();
}
}
}
async function handleBookmarkSubmit(event) {
event.preventDefault();
if (!bookmarkForm) {
return;
}
const query = bookmarkQueryInput ? bookmarkQueryInput.value.trim() : '';
const name = bookmarkNameInput ? bookmarkNameInput.value.trim() : '';
if (!query) {
resetBookmarkForm();
if (bookmarkQueryInput) {
bookmarkQueryInput.focus();
}
return;
}
bookmarkState.error = null;
try {
const response = await apiFetch(`${API_URL}/bookmarks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
label: name,
query
})
});
if (response.status === 409) {
bookmarkState.error = 'Bookmark existiert bereits.';
renderBookmarks();
resetBookmarkForm();
if (bookmarkQueryInput) {
bookmarkQueryInput.focus();
}
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const created = await response.json();
upsertBookmarkInState(created);
bookmarkState.error = null;
renderBookmarks();
resetBookmarkForm();
} catch (error) {
console.warn('Konnte Bookmark nicht speichern:', error);
bookmarkState.error = 'Bookmark konnte nicht gespeichert werden.';
renderBookmarks();
}
}
function initializeBookmarks() {
if (!bookmarksList) {
return;
}
refreshBookmarks().catch(() => {});
if (bookmarkPanel) {
bookmarkPanel.setAttribute('aria-hidden', 'true');
bookmarkPanel.hidden = true;
bookmarkPanel.style.display = 'none';
}
if (bookmarkPanelToggle) {
bookmarkPanelToggle.addEventListener('click', () => {
toggleBookmarkPanel();
});
}
if (bookmarkPanelClose) {
bookmarkPanelClose.addEventListener('click', () => {
toggleBookmarkPanel(false);
});
}
if (bookmarkCancelBtn) {
bookmarkCancelBtn.addEventListener('click', () => {
resetBookmarkForm();
if (bookmarkQueryInput) {
bookmarkQueryInput.focus();
}
});
}
if (bookmarkForm) {
bookmarkForm.addEventListener('submit', handleBookmarkSubmit);
}
if (bookmarkSearchInput) {
bookmarkSearchInput.value = bookmarkSearchTerm;
bookmarkSearchInput.addEventListener('input', () => {
bookmarkSearchTerm = typeof bookmarkSearchInput.value === 'string'
? bookmarkSearchInput.value.trim()
: '';
persistBookmarkPreferences();
renderBookmarks();
});
}
if (bookmarkSortSelect) {
bookmarkSortSelect.value = bookmarkSortMode;
bookmarkSortSelect.addEventListener('change', () => {
const value = bookmarkSortSelect.value;
if (value === 'label' || value === 'recent') {
bookmarkSortMode = value;
persistBookmarkPreferences();
renderBookmarks();
}
});
}
if (bookmarkSortDirectionToggle) {
bookmarkSortDirectionToggle.addEventListener('click', () => {
bookmarkSortDirection = bookmarkSortDirection === 'desc' ? 'asc' : 'desc';
updateBookmarkSortDirectionUI();
persistBookmarkPreferences();
renderBookmarks();
});
updateBookmarkSortDirectionUI();
}
}
function getSortSettingsPageKey() {
try {
const path = window.location.pathname;
if (typeof path === 'string' && path) {
return path;
}
} catch (error) {
// ignore and fall back
}
return 'default';
}
function updateSortDirectionToggleUI() {
if (!sortDirectionToggle) {
return;
}
const isAsc = sortDirection === 'asc';
sortDirectionToggle.setAttribute('aria-pressed', isAsc ? 'true' : 'false');
sortDirectionToggle.setAttribute('aria-label', isAsc ? 'Aufsteigend' : 'Absteigend');
sortDirectionToggle.title = isAsc ? 'Aufsteigend' : 'Absteigend';
sortDirectionToggle.dataset.direction = sortDirection;
const icon = sortDirectionToggle.querySelector('.sort-direction-toggle__icon');
if (icon) {
icon.textContent = isAsc ? '▲' : '▼';
} else {
sortDirectionToggle.textContent = isAsc ? '▲' : '▼';
}
}
function normalizeRequiredProfiles(post) {
if (Array.isArray(post.required_profiles) && post.required_profiles.length) {
return post.required_profiles
.map((value) => {
const parsed = parseInt(value, 10);
if (Number.isNaN(parsed)) {
return null;
}
return Math.min(MAX_PROFILES, Math.max(1, parsed));
})
.filter(Boolean);
}
const parsedTarget = parseInt(post.target_count, 10);
const count = Number.isNaN(parsedTarget)
? 1
: Math.min(MAX_PROFILES, Math.max(1, parsedTarget));
return Array.from({ length: count }, (_, index) => index + 1);
}
function updateTabButtons() {
document.querySelectorAll('.tab-btn').forEach((button) => {
if (!button.dataset.tab) {
return;
}
button.classList.toggle('active', button.dataset.tab === currentTab);
});
}
function isPostsViewActive() {
const postsView = document.querySelector('[data-view="posts"]');
return Boolean(postsView && postsView.classList.contains('app-view--active'));
}
function updateTabInUrl() {
if (!isPostsViewActive()) {
return;
}
const url = new URL(window.location.href);
if (currentTab === 'pending') {
url.searchParams.set('tab', 'pending');
} else {
url.searchParams.set('tab', 'all');
}
const nextState = { ...(window.history.state || {}), view: 'posts' };
window.history.replaceState(nextState, document.title, `${url.pathname}?${url.searchParams.toString()}${url.hash}`);
}
function getTabKey(tab = currentTab) {
if (tab === 'all') {
return 'all';
}
return 'pending';
}
function getVisibleCount(tab = currentTab) {
const key = getTabKey(tab);
const value = tabVisibleCounts[key];
const normalized = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : INITIAL_POST_LIMIT;
return Math.max(INITIAL_POST_LIMIT, normalized);
}
function setVisibleCount(tab, count) {
const key = getTabKey(tab);
const normalized = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : INITIAL_POST_LIMIT;
tabVisibleCounts[key] = Math.max(INITIAL_POST_LIMIT, normalized);
}
function resetVisibleCount(tab = currentTab) {
setVisibleCount(tab, INITIAL_POST_LIMIT);
}
function updateFilteredCount(tab, count) {
const key = getTabKey(tab);
tabFilteredCounts[key] = Math.max(0, count || 0);
}
function cleanupLoadMoreObserver() {
if (loadMoreObserver && observedLoadMoreElement) {
loadMoreObserver.unobserve(observedLoadMoreElement);
observedLoadMoreElement = null;
}
}
function getTabDisplayLabel(tab = currentTab) {
if (tab === 'all') {
return 'Alle Beiträge';
}
return 'Offene Beiträge';
}
function buildPostsSummary({
tab,
visibleCount,
filteredCount,
tabTotalCount,
totalCountAll,
searchActive
}) {
const label = getTabDisplayLabel(tab);
const segments = [];
segments.push(`Angezeigt: ${visibleCount} von ${filteredCount}`);
if (searchActive) {
segments.push(`Treffer: ${filteredCount} von ${tabTotalCount}`);
}
segments.push(`Tab gesamt: ${tabTotalCount}`);
segments.push(`Alle Beiträge: ${totalCountAll}`);
return `
${label}
${segments.join('·')}
`;
}
function ensureLoadMoreObserver() {
if (loadMoreObserver) {
return loadMoreObserver;
}
loadMoreObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
const element = entry.target;
const tab = element && element.dataset ? element.dataset.tab : currentTab;
if (loadMoreObserver) {
loadMoreObserver.unobserve(element);
}
loadMorePosts(tab, { triggeredByScroll: true });
});
}, {
root: null,
rootMargin: '200px 0px',
threshold: 0.1
});
return loadMoreObserver;
}
function observeLoadMoreElement(element, tab) {
if (!element) {
return;
}
const observer = ensureLoadMoreObserver();
observedLoadMoreElement = element;
element.dataset.tab = getTabKey(tab);
observer.observe(element);
}
function loadMorePosts(tab = currentTab, { triggeredByScroll = false } = {}) {
const key = getTabKey(tab);
const total = tabFilteredCounts[key] || 0;
const currentLimit = getVisibleCount(tab);
if (currentLimit >= total) {
return;
}
const newLimit = Math.min(total, currentLimit + POST_LOAD_INCREMENT);
setVisibleCount(tab, newLimit);
if (tab === currentTab) {
renderPosts();
}
}
function setTab(tab, { updateUrl = true } = {}) {
if (tab === 'all') {
currentTab = 'all';
} else {
currentTab = 'pending';
}
updateTabButtons();
loadSortMode({ fromTabChange: true });
if (updateUrl) {
updateTabInUrl();
}
renderPosts();
}
function initializeTabFromUrl() {
let tabResolved = false;
try {
const params = new URLSearchParams(window.location.search);
const tabParam = params.get('tab');
if (tabParam === 'all' || tabParam === 'pending') {
currentTab = tabParam;
tabResolved = true;
} else if (initialTabOverride) {
currentTab = initialTabOverride;
tabResolved = true;
} else if (initialViewParam === 'dashboard') {
currentTab = 'pending';
tabResolved = true;
}
} catch (error) {
console.warn('Konnte Tab-Parameter nicht auslesen:', error);
}
updateTabButtons();
if (tabResolved) {
updateTabInUrl();
}
}
function normalizeDeadlineInput(value) {
if (!value) {
return null;
}
try {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date.toISOString();
} catch (error) {
console.warn('Ungültige Deadline-Eingabe:', error);
return null;
}
}
function toDateTimeLocalValue(value) {
if (!value) {
return '';
}
try {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
return local.toISOString().slice(0, 16);
} catch (error) {
console.warn('Kann Deadline nicht für Eingabe formatieren:', error);
return '';
}
}
function normalizeFacebookPostUrl(rawValue) {
if (typeof rawValue !== 'string') {
return null;
}
let value = rawValue.trim();
if (!value) {
return null;
}
const trackingIndex = value.indexOf('__cft__');
if (trackingIndex !== -1) {
value = value.slice(0, trackingIndex);
}
value = value.replace(/[?&]$/, '');
let parsed;
try {
parsed = new URL(value);
} catch (error) {
try {
parsed = new URL(value, window.location.origin);
} catch (innerError) {
try {
parsed = new URL(value, 'https://www.facebook.com');
} catch (fallbackError) {
return null;
}
}
}
if (!parsed.hostname.toLowerCase().endsWith('facebook.com')) {
return null;
}
const normalizedPathBeforeTrim = parsed.pathname.replace(/\/+$/, '') || '/';
const lowerPathBeforeTrim = normalizedPathBeforeTrim.toLowerCase();
const watchId = parsed.searchParams.get('v') || parsed.searchParams.get('video_id');
if ((lowerPathBeforeTrim === '/watch' || lowerPathBeforeTrim === '/video.php') && watchId) {
parsed.pathname = `/reel/${watchId}/`;
parsed.search = '';
} else {
const reelMatch = lowerPathBeforeTrim.match(/^\/reel\/([^/]+)$/);
if (reelMatch) {
parsed.pathname = `/reel/${reelMatch[1]}/`;
parsed.search = '';
}
}
const cleanedParams = new URLSearchParams();
parsed.searchParams.forEach((paramValue, paramKey) => {
const lowerKey = paramKey.toLowerCase();
const isTrackingParam = FACEBOOK_TRACKING_PARAMS.some((trackingKey) => lowerKey.startsWith(trackingKey));
const isSingleUnitParam = lowerKey === 's' && paramValue === 'single_unit';
if (isTrackingParam || isSingleUnitParam) {
return;
}
cleanedParams.append(paramKey, paramValue);
});
const normalizedPath = parsed.pathname.replace(/\/+$/, '').toLowerCase();
if (normalizedPath.startsWith('/hashtag/') || normalizedPath.startsWith('/watch/hashtag/')) {
return '';
}
const search = cleanedParams.toString();
const formatted = `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ''}`;
return formatted.replace(/[?&]$/, '');
}
function getDeadlinePartsFromValue(value) {
const localValue = toDateTimeLocalValue(value);
if (!localValue) {
return {
date: '',
time: ''
};
}
const [datePart, timePart = ''] = localValue.split('T');
return {
date: datePart,
time: timePart
};
}
function getDefaultDeadlineParts() {
const tomorrow = new Date();
tomorrow.setHours(0, 0, 0, 0);
tomorrow.setDate(tomorrow.getDate() + 1);
const localIso = new Date(tomorrow.getTime() - tomorrow.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16);
const [datePart, timePart = '00:00'] = localIso.split('T');
return {
date: datePart,
time: timePart || '00:00'
};
}
function getDefaultDeadlineInputValue() {
const { date, time } = getDefaultDeadlineParts();
if (!date) {
return '';
}
return `${date}T${time || '00:00'}`;
}
function positionDeadlinePicker(picker, triggerElement) {
if (!picker || !triggerElement) {
return;
}
const rect = triggerElement.getBoundingClientRect();
const pickerRect = picker.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const safeMargin = 16;
const offset = 8;
let left = rect.left;
let top = rect.bottom + offset;
if (left + pickerRect.width + safeMargin > viewportWidth) {
left = viewportWidth - pickerRect.width - safeMargin;
}
if (left < safeMargin) {
left = safeMargin;
}
if (top + pickerRect.height + safeMargin > viewportHeight) {
top = rect.top - pickerRect.height - offset;
if (top < safeMargin) {
top = viewportHeight - pickerRect.height - safeMargin;
}
}
if (top < safeMargin) {
top = safeMargin;
}
picker.style.left = `${Math.round(left)}px`;
picker.style.top = `${Math.round(top)}px`;
}
function closeActiveDeadlinePicker() {
if (!activeDeadlinePicker) {
return;
}
const current = activeDeadlinePicker;
activeDeadlinePicker = null;
try {
if (typeof current.destroy === 'function') {
current.destroy();
return;
}
const { input } = current;
if (input) {
input.removeEventListener('change', current.onChange);
input.removeEventListener('blur', current.onBlur);
input.remove();
}
} catch (error) {
console.warn('Konnte Deadline-Picker nicht schließen:', error);
}
}
function openNativeDeadlinePicker(post, triggerElement) {
if (!post || !triggerElement) {
return;
}
closeActiveDeadlinePicker();
const picker = document.createElement('div');
picker.className = 'deadline-picker';
picker.setAttribute('role', 'dialog');
picker.setAttribute('aria-modal', 'true');
picker.setAttribute('tabindex', '-1');
picker.innerHTML = `
`;
const form = picker.querySelector('.deadline-picker__form');
const dateInput = picker.querySelector('.deadline-picker__date');
const timeInput = picker.querySelector('.deadline-picker__time');
const errorEl = picker.querySelector('.deadline-picker__error');
const cancelButton = picker.querySelector('.deadline-picker__cancel');
const closeButton = picker.querySelector('.deadline-picker__close');
const saveButton = picker.querySelector('.deadline-picker__save');
const originalSaveLabel = saveButton ? saveButton.textContent : '';
const initialValues = { date: '', time: '' };
const applyInitialValues = () => {
const existing = getDeadlinePartsFromValue(post.deadline_at);
const defaults = existing.date ? existing : getDefaultDeadlineParts();
initialValues.date = defaults.date;
initialValues.time = defaults.time || '';
if (dateInput) {
dateInput.value = defaults.date;
}
if (timeInput) {
timeInput.value = defaults.time;
}
};
applyInitialValues();
document.body.appendChild(picker);
positionDeadlinePicker(picker, triggerElement);
const showError = (message = '') => {
if (!errorEl) {
return;
}
errorEl.textContent = message;
errorEl.classList.toggle('is-visible', Boolean(message));
};
let isSaving = false;
const normalizeParts = (datePart, timePart) => {
if (!datePart) {
return '';
}
return `${datePart}T${(timePart || '00:00')}`;
};
const attemptAutoSave = () => {
if (isSaving || !form || !dateInput) {
return false;
}
const currentDate = dateInput.value;
if (!currentDate) {
return false;
}
const currentTime = timeInput ? timeInput.value : '';
const currentNormalized = normalizeParts(currentDate, currentTime);
const initialNormalized = normalizeParts(initialValues.date, initialValues.time);
if (currentNormalized === initialNormalized) {
return false;
}
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
} else {
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
return true;
};
const handleSubmit = async (event) => {
event.preventDefault();
if (isSaving || !dateInput || !saveButton) {
return;
}
showError('');
const dateValue = dateInput.value;
if (!dateValue) {
showError('Bitte ein Datum auswählen.');
dateInput.focus();
return;
}
const timeValue = (timeInput && timeInput.value) ? timeInput.value : '00:00';
const combined = `${dateValue}T${timeValue || '00:00'}`;
const normalized = normalizeDeadlineInput(combined);
if (!normalized) {
showError('Ungültige Eingabe.');
return;
}
try {
isSaving = true;
saveButton.disabled = true;
saveButton.textContent = 'Speichert…';
const success = await saveDeadline(post.id, normalized);
if (success) {
closeActiveDeadlinePicker();
return;
}
showError('Deadline wurde nicht gespeichert.');
} catch (error) {
console.warn('Deadline konnte nicht gespeichert werden:', error);
showError('Konnte Deadline nicht speichern.');
} finally {
isSaving = false;
saveButton.disabled = false;
saveButton.textContent = originalSaveLabel;
}
};
form?.addEventListener('submit', handleSubmit);
const cleanupListeners = [];
const pickerState = {
destroy: () => {
while (cleanupListeners.length) {
const clean = cleanupListeners.pop();
try {
clean();
} catch (error) {
console.warn('Konnte Listener nicht entfernen:', error);
}
}
picker.remove();
}
};
const registerListener = (target, type, listener, options) => {
if (!target) {
return;
}
target.addEventListener(type, listener, options);
cleanupListeners.push(() => target.removeEventListener(type, listener, options));
};
const closePicker = () => {
if (activeDeadlinePicker) {
closeActiveDeadlinePicker();
return;
}
pickerState.destroy();
};
registerListener(cancelButton, 'click', (event) => {
event.preventDefault();
closePicker();
});
registerListener(closeButton, 'click', (event) => {
event.preventDefault();
closePicker();
});
const handleOutsidePointer = (event) => {
const targetElement = event.target instanceof Element ? event.target : null;
const isClearButton = targetElement?.closest('.post-deadline__clear');
if (!picker.contains(event.target) && !triggerElement.contains(event.target)) {
if (!isClearButton && attemptAutoSave()) {
event.preventDefault();
event.stopPropagation();
return;
}
closePicker();
}
};
const handleKeydown = (event) => {
if (event.key === 'Escape') {
event.preventDefault();
closePicker();
}
};
const reposition = () => {
positionDeadlinePicker(picker, triggerElement);
};
registerListener(document, 'pointerdown', handleOutsidePointer, true);
registerListener(document, 'keydown', handleKeydown);
registerListener(window, 'resize', reposition);
registerListener(window, 'scroll', reposition, true);
activeDeadlinePicker = pickerState;
requestAnimationFrame(() => {
positionDeadlinePicker(picker, triggerElement);
requestAnimationFrame(() => {
dateInput?.focus({ preventScroll: true });
});
});
}
async function clearDeadline(postId) {
closeActiveDeadlinePicker();
await saveDeadline(postId, null);
}
function normalizeChecks(checks) {
if (!Array.isArray(checks)) {
return [];
}
return checks
.map((check) => {
if (!check) {
return null;
}
const parsed = parseInt(check.profile_number, 10);
if (Number.isNaN(parsed)) {
return null;
}
const profileNumber = Math.min(MAX_PROFILES, Math.max(1, parsed));
return {
...check,
profile_number: profileNumber,
profile_name: check.profile_name || getProfileName(profileNumber),
checked_at: check.checked_at || null
};
})
.filter(Boolean)
.sort((a, b) => {
const aTime = a.checked_at ? new Date(a.checked_at).getTime() : 0;
const bTime = b.checked_at ? new Date(b.checked_at).getTime() : 0;
if (aTime === bTime) {
return a.profile_number - b.profile_number;
}
return aTime - bTime;
});
}
function calculateUrgencyScore(postItem) {
const post = postItem.post;
const status = postItem.status;
// Remaining participations needed
const remaining = status.targetCount - status.checkedCount;
if (remaining <= 0) return 999999; // Completed posts go to bottom
const now = Date.now();
const createdAt = post.created_at ? new Date(post.created_at).getTime() : now;
// Time until deadline (in hours)
let hoursUntilDeadline = Infinity;
let totalDuration = Infinity;
if (post.deadline_at) {
const deadline = new Date(post.deadline_at).getTime();
hoursUntilDeadline = Math.max(0, (deadline - now) / (1000 * 60 * 60));
totalDuration = Math.max(1, (deadline - createdAt) / (1000 * 60 * 60)); // Total hours from creation to deadline
}
// Time since last participation (in hours)
let hoursSinceLastCheck = Infinity;
if (status.lastCheckedAt) {
const lastCheck = new Date(status.lastCheckedAt).getTime();
hoursSinceLastCheck = (now - lastCheck) / (1000 * 60 * 60);
}
// Calculate ideal pace: how often should participations happen?
let idealIntervalHours = Infinity;
let behindSchedule = false;
if (totalDuration < Infinity && status.targetCount > 0) {
// Ideal interval between participations
idealIntervalHours = totalDuration / status.targetCount;
// Expected participations by now (based on time elapsed)
const hoursElapsed = (now - createdAt) / (1000 * 60 * 60);
const expectedChecks = Math.floor(hoursElapsed / idealIntervalHours);
// Are we behind schedule?
behindSchedule = status.checkedCount < expectedChecks;
}
// Calculate urgency score (lower = more urgent)
let score = 0;
// For posts with deadline: calculate when the next participation should ideally happen
if (hoursUntilDeadline < Infinity && remaining > 0) {
// How many hours do we have per remaining participation?
const hoursPerParticipation = hoursUntilDeadline / remaining;
// When should the next participation ideally happen?
let hoursUntilNextIdeal = hoursPerParticipation;
// If we have a last check, calculate from there
if (hoursSinceLastCheck < Infinity) {
hoursUntilNextIdeal = Math.max(0, hoursPerParticipation - hoursSinceLastCheck);
}
// Score based on how soon the next participation is due
// Posts that are overdue or due soon get higher priority
if (hoursUntilNextIdeal <= 0) {
// Overdue! High priority
score = Math.abs(hoursUntilNextIdeal) * -10; // Negative score = very high priority
} else if (hoursUntilNextIdeal <= 24) {
// Due within 24h
score = hoursUntilNextIdeal * 10; // 0-240
} else if (hoursUntilNextIdeal <= 72) {
// Due within 3 days
score = 240 + (hoursUntilNextIdeal - 24) * 20; // 240-1200
} else {
// Due later
score = 1200 + (hoursUntilNextIdeal - 72) * 50; // 1200+
}
// Emergency boost: if deadline is very close, override everything
if (hoursUntilDeadline <= 24 && remaining > 0) {
score = Math.min(score, hoursUntilDeadline * 5); // Max 120 points
} else if (hoursUntilDeadline <= 48 && remaining > 1) {
score = Math.min(score, 120 + (hoursUntilDeadline - 24) * 10);
}
} else if (hoursUntilDeadline < Infinity && remaining === 0) {
// Completed with deadline
score = 100000;
} else {
// No deadline - use simpler heuristic
score = 50000;
// Prioritize posts that haven't been checked in a while
if (hoursSinceLastCheck < Infinity) {
if (hoursSinceLastCheck < 24) {
score += 50; // Recently checked = lower priority
} else if (hoursSinceLastCheck > 72) {
score -= 100; // Long time since check = higher priority
}
}
}
// Recent participation (<24h) should lower urgency regardless of deadline pressure
if (hoursSinceLastCheck < 24) {
const freshnessRatio = (24 - hoursSinceLastCheck) / 24; // 0 (at 24h) to 1 (just now)
const recencyPenalty = 60 + Math.round(freshnessRatio * 120); // 60-180 point penalty
score += recencyPenalty;
}
// Give extra priority to posts with deadlines approaching soon
if (hoursUntilDeadline < Infinity && remaining > 0) {
const urgencyWindowHours = 72;
const cappedHours = Math.min(hoursUntilDeadline, urgencyWindowHours);
const closenessRatio = 1 - (cappedHours / urgencyWindowHours); // 0-1
if (closenessRatio > 0) {
const baseBoost = Math.round(closenessRatio * 120); // Up to 120 points
const remainingFactor = Math.min(1.6, 0.7 + remaining * 0.3); // 1.0 (1 remaining) .. 1.6 (>=3 remaining)
const deadlineBoost = Math.round(baseBoost * remainingFactor);
score -= deadlineBoost;
}
}
return score;
}
function comparePostItems(a, b) {
const postA = a.post;
const postB = b.post;
const createdA = toTimestamp(postA.created_at, 0);
const createdB = toTimestamp(postB.created_at, 0);
let comparison = 0;
if (sortMode === 'deadline') {
const deadlineA = toTimestamp(postA.deadline_at, Infinity);
const deadlineB = toTimestamp(postB.deadline_at, Infinity);
if (deadlineA !== deadlineB) {
comparison = deadlineA - deadlineB;
}
} else if (sortMode === 'smart') {
// Smart sorting based on urgency
const scoreA = calculateUrgencyScore(a);
const scoreB = calculateUrgencyScore(b);
if (scoreA !== scoreB) {
comparison = scoreB - scoreA; // Higher score = lower priority (inverted for intuitive direction)
}
} else if (sortMode === 'lastChange') {
const changeA = Number.isFinite(a.status.lastChangeTimestamp)
? a.status.lastChangeTimestamp
: toTimestamp(postA.created_at, 0);
const changeB = Number.isFinite(b.status.lastChangeTimestamp)
? b.status.lastChangeTimestamp
: toTimestamp(postB.created_at, 0);
if (changeA !== changeB) {
comparison = changeA - changeB;
}
} else if (sortMode === 'lastCheck') {
const lastA = Number.isFinite(a.status.lastCheckedTimestamp)
? a.status.lastCheckedTimestamp
: Number.NEGATIVE_INFINITY;
const lastB = Number.isFinite(b.status.lastCheckedTimestamp)
? b.status.lastCheckedTimestamp
: Number.NEGATIVE_INFINITY;
if (lastA !== lastB) {
comparison = lastA - lastB;
}
} else if (createdA !== createdB) {
comparison = createdA - createdB;
}
if (comparison === 0 && createdA !== createdB) {
comparison = createdA - createdB;
}
if (comparison === 0) {
const idA = String(postA.id || '');
const idB = String(postB.id || '');
if (idA !== idB) {
comparison = idA < idB ? -1 : 1;
}
}
if (comparison === 0) {
return 0;
}
const multiplier = sortDirection === 'asc' ? 1 : -1;
return comparison * multiplier;
}
function displayManualPostMessage(message, type = 'success') {
if (!manualPostMessage) {
return;
}
manualPostMessage.textContent = message;
manualPostMessage.classList.remove('error', 'success');
manualPostMessage.classList.add(type);
}
function clearManualPostMessage() {
if (!manualPostMessage) {
return;
}
manualPostMessage.textContent = '';
manualPostMessage.classList.remove('error', 'success');
}
function openManualPostModal({ mode = 'create', post = null, focus = null } = {}) {
if (!manualPostModal || !manualPostForm) {
return;
}
manualPostModalLastFocus = document.activeElement && typeof document.activeElement.blur === 'function'
? document.activeElement
: null;
resetManualPostForm({ keepMessages: false });
manualPostMode = mode === 'edit' ? 'edit' : 'create';
manualPostEditingId = manualPostMode === 'edit' && post ? post.id : null;
const desiredFocus = typeof focus === 'string' ? focus : (manualPostMode === 'edit' ? 'title' : 'url');
if (manualPostModalTitle) {
manualPostModalTitle.textContent = manualPostMode === 'edit'
? 'Beitrag bearbeiten'
: 'Beitrag hinzufügen';
}
if (manualPostSubmitButton) {
manualPostSubmitButton.textContent = manualPostMode === 'edit'
? 'Aktualisieren'
: 'Speichern';
}
if (manualPostMode === 'edit' && post) {
populateManualPostForm(post);
}
manualPostModal.removeAttribute('hidden');
manualPostModal.classList.add('open');
manualPostModalPreviousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
requestAnimationFrame(() => {
if (manualPostModalContent) {
manualPostModalContent.focus();
}
if (desiredFocus === 'deadline' && manualPostDeadlineInput) {
manualPostDeadlineInput.focus();
manualPostDeadlineInput.select?.();
return;
}
if (desiredFocus === 'title' && manualPostTitleInput) {
manualPostTitleInput.focus();
manualPostTitleInput.select();
return;
}
if (desiredFocus === 'url' && manualPostUrlInput) {
manualPostUrlInput.focus();
manualPostUrlInput.select?.();
return;
}
if (manualPostMode === 'edit' && manualPostTitleInput) {
manualPostTitleInput.focus();
manualPostTitleInput.select();
} else if (manualPostUrlInput) {
manualPostUrlInput.focus();
}
});
}
function closeManualPostModal() {
if (!manualPostModal) {
return;
}
manualPostModal.classList.remove('open');
manualPostModal.setAttribute('hidden', '');
resetManualPostForm();
document.body.style.overflow = manualPostModalPreviousOverflow;
if (manualPostModalLastFocus && typeof manualPostModalLastFocus.focus === 'function') {
manualPostModalLastFocus.focus();
}
}
function loadAutoRefreshSettings() {
try {
const stored = localStorage.getItem(REFRESH_SETTINGS_KEY);
if (stored) {
const parsed = JSON.parse(stored);
if (typeof parsed === 'object' && parsed) {
if (typeof parsed.enabled === 'boolean') {
autoRefreshSettings.enabled = parsed.enabled;
}
if (typeof parsed.interval === 'number' && parsed.interval >= 5000) {
autoRefreshSettings.interval = parsed.interval;
}
}
}
} catch (error) {
console.warn('Konnte Refresh-Einstellungen nicht laden:', error);
}
if (autoRefreshToggle) {
autoRefreshToggle.checked = autoRefreshSettings.enabled;
}
if (autoRefreshIntervalSelect) {
autoRefreshIntervalSelect.value = String(autoRefreshSettings.interval);
autoRefreshIntervalSelect.disabled = !autoRefreshSettings.enabled;
}
}
function saveAutoRefreshSettings() {
try {
localStorage.setItem(REFRESH_SETTINGS_KEY, JSON.stringify(autoRefreshSettings));
} catch (error) {
console.warn('Konnte Refresh-Einstellungen nicht speichern:', error);
}
}
function applyAutoRefreshSettings() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
if (autoRefreshIntervalSelect) {
const disabled = !autoRefreshSettings.enabled || updatesStreamHealthy;
autoRefreshIntervalSelect.disabled = disabled;
autoRefreshIntervalSelect.title = updatesStreamHealthy
? 'Live-Updates sind aktiv; das Intervall wird aktuell nicht verwendet.'
: '';
}
if (!autoRefreshSettings.enabled) {
return;
}
if (updatesStreamHealthy) {
return;
}
autoRefreshTimer = setInterval(() => {
if (document.hidden) {
return;
}
fetchPosts({ showLoader: false });
}, autoRefreshSettings.interval);
}
function loadSortMode({ fromTabChange = false } = {}) {
const pageKey = getSortSettingsPageKey();
const tabKey = getSortTabKey();
const storage = getSortStorage();
const pageSettings = storage[pageKey] && typeof storage[pageKey] === 'object' ? storage[pageKey] : {};
const legacyCandidate = (pageSettings.mode || pageSettings.direction)
? { mode: pageSettings.mode, direction: pageSettings.direction }
: null;
const candidates = [pageSettings[tabKey], legacyCandidate, pageSettings.default, storage.default];
let applied = null;
for (const candidate of candidates) {
if (candidate && typeof candidate === 'object') {
applied = candidate;
break;
}
}
if (applied) {
sortMode = normalizeSortMode(applied.mode);
sortDirection = normalizeSortDirection(applied.direction);
} else {
sortMode = normalizeSortMode(sortMode);
sortDirection = normalizeSortDirection(sortDirection);
}
if (sortModeSelect) {
sortModeSelect.value = sortMode;
}
updateSortDirectionToggleUI();
if (!fromTabChange) {
// Ensure structure exists for initial load
saveSortMode();
}
}
function saveSortMode() {
const pageKey = getSortSettingsPageKey();
const tabKey = getSortTabKey();
const storage = getSortStorage();
if (!storage[pageKey] || typeof storage[pageKey] !== 'object') {
storage[pageKey] = {};
}
delete storage[pageKey].mode;
delete storage[pageKey].direction;
storage[pageKey][tabKey] = {
mode: normalizeSortMode(sortMode),
direction: normalizeSortDirection(sortDirection)
};
if (!storage[pageKey].default) {
storage[pageKey].default = { ...DEFAULT_SORT_SETTINGS };
}
if (!storage.default) {
storage.default = { ...DEFAULT_SORT_SETTINGS };
}
persistSortStorage(storage);
}
function computePostStatus(post, profileNumber = currentProfile) {
const requiredProfiles = normalizeRequiredProfiles(post);
const checks = normalizeChecks(post.checks);
let lastCheckedAt = null;
let lastCheckedTimestamp = null;
for (const check of checks) {
if (!check || !check.checked_at) {
continue;
}
const timestamp = new Date(check.checked_at).getTime();
if (Number.isNaN(timestamp)) {
continue;
}
if (lastCheckedTimestamp === null || timestamp > lastCheckedTimestamp) {
lastCheckedTimestamp = timestamp;
lastCheckedAt = check.checked_at;
}
}
const lastChangeTimestamp = toTimestamp(post.last_change, toTimestamp(post.created_at, 0));
const lastChangeAt = post.last_change || post.created_at || null;
// Check if post is expired (deadline passed)
const isExpired = post.deadline_at ? new Date(post.deadline_at) < new Date() : false;
const backendStatuses = Array.isArray(post.profile_statuses) ? post.profile_statuses : [];
let profileStatuses = backendStatuses
.map((status) => {
if (!status) {
return null;
}
const parsed = parseInt(status.profile_number, 10);
if (Number.isNaN(parsed)) {
return null;
}
const profileNumberValue = Math.min(MAX_PROFILES, Math.max(1, parsed));
let normalizedStatus = status.status;
if (normalizedStatus !== 'done' && normalizedStatus !== 'available') {
normalizedStatus = 'locked';
}
const check = checks.find((item) => item.profile_number === profileNumberValue) || null;
return {
profile_number: profileNumberValue,
profile_name: status.profile_name || getProfileName(profileNumberValue),
status: normalizedStatus,
checked_at: status.checked_at || (check ? check.checked_at : null) || null
};
})
.filter(Boolean);
if (profileStatuses.length !== requiredProfiles.length) {
const checksByProfile = new Map(checks.map((check) => [check.profile_number, check]));
const completedSet = new Set(checks.map((check) => check.profile_number));
profileStatuses = requiredProfiles.map((value, index) => {
const prerequisites = requiredProfiles.slice(0, index);
const prerequisitesMet = prerequisites.every((profile) => completedSet.has(profile));
const check = checksByProfile.get(value) || null;
return {
profile_number: value,
profile_name: getProfileName(value),
status: check ? 'done' : (prerequisitesMet ? 'available' : 'locked'),
checked_at: check ? check.checked_at : null
};
});
} else {
const checksByProfile = new Map(checks.map((check) => [check.profile_number, check]));
profileStatuses = requiredProfiles.map((value) => {
const status = profileStatuses.find((item) => item.profile_number === value);
if (!status) {
const check = checksByProfile.get(value) || null;
return {
profile_number: value,
profile_name: getProfileName(value),
status: check ? 'done' : 'locked',
checked_at: check ? check.checked_at : null
};
}
if (status.status === 'done') {
const check = checksByProfile.get(value) || null;
return {
...status,
checked_at: status.checked_at || (check ? check.checked_at : null) || null
};
}
if (status.status === 'available') {
return {
...status,
checked_at: status.checked_at || null
};
}
return {
...status,
status: 'locked',
checked_at: status.checked_at || null
};
});
}
const completedProfilesSet = new Set(
profileStatuses
.filter((status) => status.status === 'done')
.map((status) => status.profile_number)
);
const checkedCount = profileStatuses.filter((status) => status.status === 'done').length;
const targetCount = profileStatuses.length;
const isComplete = profileStatuses.every((status) => status.status === 'done');
const nextRequiredProfile = profileStatuses.find((status) => status.status === 'available') || null;
const isCurrentProfileRequired = requiredProfiles.includes(profileNumber);
const isCurrentProfileDone = profileStatuses.some((status) => status.profile_number === profileNumber && status.status === 'done');
const canCurrentProfileCheck = profileStatuses.some((status) => status.profile_number === profileNumber && status.status === 'available');
const waitingForStatuses = profileStatuses.filter((status) => status.profile_number < profileNumber && status.status !== 'done');
const waitingForProfiles = waitingForStatuses.map((status) => status.profile_number);
const waitingForNames = waitingForStatuses.map((status) => status.profile_name);
return {
requiredProfiles,
profileStatuses,
checks,
lastCheckedAt,
lastCheckedTimestamp,
lastChangeAt,
lastChangeTimestamp,
completedProfilesSet,
checkedCount,
targetCount,
isComplete,
isCurrentProfileRequired,
isCurrentProfileDone,
canCurrentProfileCheck,
waitingForProfiles,
waitingForNames,
nextRequiredProfile,
profileNumber,
isExpired
};
}
function applyScreenshotModalSize() {
if (!screenshotModalContent || !screenshotModalImage) {
return;
}
if (screenshotModalZoomed) {
return;
}
if (!screenshotModalImage.src) {
return;
}
requestAnimationFrame(() => {
const padding = 48;
const viewportWidth = Math.max(320, window.innerWidth * 0.95);
const viewportHeight = Math.max(280, window.innerHeight * 0.92);
const naturalWidth = screenshotModalImage.naturalWidth || screenshotModalImage.width || 0;
const naturalHeight = screenshotModalImage.naturalHeight || screenshotModalImage.height || 0;
const targetWidth = Math.min(Math.max(320, naturalWidth + padding), viewportWidth);
const targetHeight = Math.min(Math.max(260, naturalHeight + padding), viewportHeight);
screenshotModalContent.style.width = `${targetWidth}px`;
screenshotModalContent.style.height = `${targetHeight}px`;
});
}
async function fetchProfileState() {
try {
const response = await apiFetch(`${API_URL}/profile-state`);
if (!response.ok) {
return null;
}
const data = await response.json();
if (data && typeof data.profile_number !== 'undefined') {
const parsed = parseInt(data.profile_number, 10);
if (!Number.isNaN(parsed)) {
return parsed;
}
}
return null;
} catch (error) {
console.warn('Profilstatus konnte nicht geladen werden:', error);
return null;
}
}
async function pushProfileState(profileNumber) {
try {
await apiFetch(`${API_URL}/profile-state`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile_number: profileNumber })
});
} catch (error) {
console.error('Profilstatus konnte nicht gespeichert werden:', error);
}
}
function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
if (!profileNumber) {
return;
}
if (profileSelectElement) {
profileSelectElement.value = String(profileNumber);
}
if (currentProfile === profileNumber) {
if (!fromBackend) {
pushProfileState(profileNumber);
}
return;
}
currentProfile = profileNumber;
localStorage.setItem('profileNumber', currentProfile);
if (!fromBackend) {
pushProfileState(currentProfile);
}
resetVisibleCount();
renderPosts();
}
// Load profile from localStorage
function loadProfile() {
fetchProfileState().then((backendProfile) => {
if (backendProfile) {
applyProfileNumber(backendProfile, { fromBackend: true });
} else {
const saved = localStorage.getItem('profileNumber');
if (saved) {
applyProfileNumber(parseInt(saved, 10) || 1, { fromBackend: true });
} else {
applyProfileNumber(1, { fromBackend: true });
}
}
});
}
// Save profile to localStorage
function saveProfile(profileNumber) {
applyProfileNumber(profileNumber);
}
function startProfilePolling() {
if (profilePollTimer) {
clearInterval(profilePollTimer);
}
profilePollTimer = setInterval(async () => {
const backendProfile = await fetchProfileState();
if (backendProfile && backendProfile !== currentProfile) {
applyProfileNumber(backendProfile, { fromBackend: true });
}
}, 5000);
}
// Profile selector change handler
if (profileSelectElement) {
profileSelectElement.addEventListener('change', (e) => {
saveProfile(parseInt(e.target.value, 10));
});
}
if (includeExpiredToggle) {
updateIncludeExpiredToggleUI();
updateIncludeExpiredToggleVisibility();
includeExpiredToggle.addEventListener('change', () => {
includeExpiredPosts = includeExpiredToggle.checked;
persistIncludeExpiredPreference(includeExpiredPosts);
resetVisibleCount('all');
renderPosts();
});
}
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
setTab(btn.dataset.tab, { updateUrl: true });
});
});
window.addEventListener('app:view-change', (event) => {
if (event && event.detail && event.detail.view === 'posts') {
updateTabInUrl();
}
});
if (manualPostForm) {
manualPostForm.addEventListener('submit', handleManualPostSubmit);
}
if (manualPostResetButton) {
manualPostResetButton.addEventListener('click', () => {
if (manualPostMode === 'edit' && manualPostEditingId) {
const post = posts.find((item) => item.id === manualPostEditingId);
if (post) {
populateManualPostForm(post);
}
} else {
resetManualPostForm();
if (manualPostUrlInput) {
manualPostUrlInput.focus();
}
}
});
}
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', () => {
autoRefreshSettings.enabled = !!autoRefreshToggle.checked;
saveAutoRefreshSettings();
applyAutoRefreshSettings();
if (autoRefreshSettings.enabled && updatesStreamHealthy) {
console.info('Live-Updates sind aktiv; automatisches Refresh bleibt pausiert.');
}
if (autoRefreshSettings.enabled && !updatesStreamHealthy) {
fetchPosts({ showLoader: false });
}
});
}
if (autoRefreshIntervalSelect) {
autoRefreshIntervalSelect.addEventListener('change', () => {
const value = parseInt(autoRefreshIntervalSelect.value, 10);
if (!Number.isNaN(value) && value >= 5000) {
autoRefreshSettings.interval = value;
saveAutoRefreshSettings();
applyAutoRefreshSettings();
}
});
}
const manualRefreshBtn = document.getElementById('manualRefreshBtn');
if (manualRefreshBtn) {
manualRefreshBtn.addEventListener('click', () => {
fetchPosts();
});
}
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', () => {
resetVisibleCount();
renderPosts();
});
}
if (sortModeSelect) {
sortModeSelect.addEventListener('change', () => {
const value = sortModeSelect.value;
sortMode = VALID_SORT_MODES.has(value) ? value : DEFAULT_SORT_SETTINGS.mode;
saveSortMode();
resetVisibleCount();
renderPosts();
});
}
if (sortDirectionToggle) {
sortDirectionToggle.addEventListener('click', () => {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
updateSortDirectionToggleUI();
saveSortMode();
resetVisibleCount();
renderPosts();
});
}
if (openManualPostModalBtn) {
openManualPostModalBtn.addEventListener('click', () => {
openManualPostModal({ mode: 'create' });
});
}
if (manualPostModalClose) {
manualPostModalClose.addEventListener('click', closeManualPostModal);
}
if (manualPostModalBackdrop) {
manualPostModalBackdrop.addEventListener('click', closeManualPostModal);
}
// Fetch all posts
async function fetchPosts({ showLoader = true } = {}) {
if (isFetchingPosts) {
return;
}
isFetchingPosts = true;
try {
if (showLoader) {
showLoading();
}
const response = await apiFetch(`${API_URL}/posts`);
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
const data = await response.json();
posts = Array.isArray(data) ? data : [];
await normalizeLoadedPostUrls();
sortPostsByCreatedAt();
renderPosts();
} catch (error) {
if (showLoader) {
showError('Fehler beim Laden der Beiträge. Stelle sicher, dass das Backend läuft.');
}
console.error('Error fetching posts:', error);
} finally {
if (showLoader) {
hideLoading();
}
isFetchingPosts = false;
}
}
async function normalizeLoadedPostUrls() {
if (!Array.isArray(posts) || !posts.length) {
return false;
}
const candidates = posts
.map((post) => {
if (!post || !post.id || !post.url) {
return null;
}
const cleaned = normalizeFacebookPostUrl(post.url);
if (!cleaned || cleaned === post.url) {
return null;
}
return { id: post.id, cleaned };
})
.filter(Boolean);
if (!candidates.length) {
return false;
}
let changed = false;
for (const candidate of candidates) {
try {
const response = await apiFetch(`${API_URL}/posts/${candidate.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: candidate.cleaned })
});
if (!response.ok) {
console.warn(`Konnte URL für Beitrag ${candidate.id} nicht normalisieren.`);
continue;
}
const updatedPost = await response.json();
posts = posts.map((item) => (item.id === candidate.id ? updatedPost : item));
changed = true;
} catch (error) {
console.warn(`Fehler beim Normalisieren der URL für Beitrag ${candidate.id}:`, error);
}
}
return changed;
}
function doesPostMatchFocus(post) {
if (!post) {
return false;
}
if (focusPostIdParam && String(post.id) === focusPostIdParam) {
return true;
}
if (focusNormalizedUrl && post.url) {
const candidateNormalized = normalizeFacebookPostUrl(post.url) || post.url;
return candidateNormalized === focusNormalizedUrl;
}
return false;
}
function resolveFocusTargetInfo(items) {
if (!Array.isArray(items) || (!focusPostIdParam && !focusNormalizedUrl)) {
return { index: -1, post: null };
}
const index = items.findIndex(({ post }) => doesPostMatchFocus(post));
return {
index,
post: index !== -1 && items[index] ? items[index].post : null
};
}
function clearFocusParamsFromUrl() {
try {
const url = new URL(window.location.href);
let changed = false;
if (url.searchParams.has('postId')) {
url.searchParams.delete('postId');
changed = true;
}
if (url.searchParams.has('postUrl')) {
url.searchParams.delete('postUrl');
changed = true;
}
if (changed) {
const newQuery = url.searchParams.toString();
const newUrl = `${url.pathname}${newQuery ? `?${newQuery}` : ''}${url.hash}`;
window.history.replaceState({}, document.title, newUrl);
}
} catch (error) {
console.warn('Konnte Fokus-Parameter nicht aus URL entfernen:', error);
}
}
function highlightPostCard(post) {
if (!post || focusHandled) {
return;
}
const card = document.getElementById(`post-${post.id}`);
if (!card) {
return;
}
card.classList.add('post-card--highlight');
const hadTabIndex = card.hasAttribute('tabindex');
if (!hadTabIndex) {
card.setAttribute('tabindex', '-1');
card.dataset.fbTrackerTempTabindex = '1';
}
requestAnimationFrame(() => {
try {
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
} catch (error) {
console.warn('Konnte Karte nicht scrollen:', error);
}
try {
card.focus({ preventScroll: true });
} catch (error) {
// ignore focus errors
}
});
window.setTimeout(() => {
card.classList.remove('post-card--highlight');
if (card.dataset.fbTrackerTempTabindex === '1') {
card.removeAttribute('tabindex');
delete card.dataset.fbTrackerTempTabindex;
}
}, 4000);
focusPostIdParam = null;
focusPostUrlParam = null;
focusNormalizedUrl = '';
focusHandled = true;
focusTabAdjusted = null;
clearFocusParamsFromUrl();
}
// Render posts
function renderPosts() {
hideLoading();
hideError();
const container = document.getElementById('postsContainer');
if (!container) {
return;
}
updateIncludeExpiredToggleVisibility();
closeActiveDeadlinePicker();
updateTabButtons();
cleanupLoadMoreObserver();
const postItems = posts.map((post) => ({
post,
status: computePostStatus(post)
}));
const sortedItems = [...postItems].sort(comparePostItems);
const focusCandidateEntry = (!focusHandled && (focusPostIdParam || focusNormalizedUrl))
? sortedItems.find((item) => doesPostMatchFocus(item.post))
: null;
let filteredItems = sortedItems;
if (currentTab === 'pending') {
filteredItems = sortedItems.filter((item) => !item.status.isExpired && item.status.canCurrentProfileCheck && !item.status.isComplete);
} else {
filteredItems = includeExpiredPosts
? sortedItems
: sortedItems.filter((item) => !item.status.isExpired && !item.status.isComplete);
}
const tabTotalCount = filteredItems.length;
const searchInput = document.getElementById('searchInput');
const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : '';
const searchActive = Boolean(searchValue);
if (searchActive) {
const searchTerm = searchValue.toLowerCase();
filteredItems = filteredItems.filter((item) => {
const post = item.post;
return (
(post.title && post.title.toLowerCase().includes(searchTerm)) ||
(post.url && post.url.toLowerCase().includes(searchTerm)) ||
(post.created_by_name && post.created_by_name.toLowerCase().includes(searchTerm)) ||
(post.id && post.id.toLowerCase().includes(searchTerm))
);
});
}
if (!focusHandled && focusCandidateEntry && !searchActive) {
const candidateVisibleInCurrentTab = filteredItems.some(({ post }) => doesPostMatchFocus(post));
if (!candidateVisibleInCurrentTab) {
if (currentTab !== 'all' && focusTabAdjusted !== 'all') {
focusTabAdjusted = 'all';
setTab('all');
return;
}
if (!includeExpiredPosts && (focusCandidateEntry.status.isExpired || focusCandidateEntry.status.isComplete)) {
includeExpiredPosts = true;
persistIncludeExpiredPreference(includeExpiredPosts);
updateIncludeExpiredToggleUI();
renderPosts();
return;
}
}
}
const focusTargetInfo = resolveFocusTargetInfo(filteredItems);
if (focusTargetInfo.index !== -1) {
const requiredVisible = focusTargetInfo.index + 1;
if (requiredVisible > getVisibleCount(currentTab)) {
setVisibleCount(currentTab, requiredVisible);
}
}
updateFilteredCount(currentTab, filteredItems.length);
const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab));
const visibleItems = filteredItems.slice(0, visibleCount);
const summaryHtml = buildPostsSummary({
tab: currentTab,
visibleCount,
filteredCount: filteredItems.length,
tabTotalCount,
totalCountAll: posts.length,
searchActive
});
if (filteredItems.length === 0) {
let emptyMessage = 'Noch keine Beiträge erfasst.';
let emptyIcon = '🎉';
if (currentTab === 'pending') {
emptyMessage = 'Keine offenen Beiträge!';
} else {
emptyMessage = 'Keine Beiträge vorhanden.';
}
if (searchActive) {
emptyMessage = 'Keine Beiträge gefunden.';
emptyIcon = '🔍';
}
container.innerHTML = `${summaryHtml}
${emptyIcon}
${emptyMessage}
`;
return;
}
container.innerHTML = `${summaryHtml}${visibleItems
.map(({ post, status }, index) => createPostCard(post, status, {
index,
globalIndex: index + 1,
totalFiltered: filteredItems.length,
totalOverall: posts.length,
tabTotalCount,
searchActive
}))
.join('')}`;
visibleItems.forEach(({ post, status }) => attachPostEventHandlers(post, status));
if (visibleCount < filteredItems.length) {
const loadMoreContainer = document.createElement('div');
loadMoreContainer.className = 'posts-load-more';
const loadMoreButton = document.createElement('button');
loadMoreButton.type = 'button';
loadMoreButton.className = 'btn btn-secondary posts-load-more__btn';
loadMoreButton.textContent = 'Weitere Beiträge laden';
loadMoreButton.addEventListener('click', () => {
loadMoreButton.disabled = true;
loadMoreButton.textContent = 'Lade...';
loadMorePosts(currentTab, { triggeredByScroll: false });
});
loadMoreContainer.appendChild(loadMoreButton);
container.appendChild(loadMoreContainer);
observeLoadMoreElement(loadMoreContainer, currentTab);
}
if (!focusHandled && focusTargetInfo.index !== -1 && focusTargetInfo.post) {
requestAnimationFrame(() => highlightPostCard(focusTargetInfo.post));
}
}
function attachPostEventHandlers(post, status) {
const card = document.getElementById(`post-${post.id}`);
if (!card) {
return;
}
const openBtn = card.querySelector('.btn-open');
if (openBtn) {
openBtn.addEventListener('click', () => openPost(post.id));
}
const deleteBtn = card.querySelector('.btn-delete');
if (deleteBtn) {
deleteBtn.addEventListener('click', () => deletePost(post.id));
}
const screenshotEl = card.querySelector('.post-screenshot');
if (screenshotEl && screenshotEl.dataset.screenshot) {
const url = screenshotEl.dataset.screenshot;
const openHandler = () => openScreenshotModal(url);
screenshotEl.addEventListener('click', openHandler);
screenshotEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openScreenshotModal(url);
}
});
}
const toggleButtons = card.querySelectorAll('.profile-line__toggle');
toggleButtons.forEach((button) => {
button.addEventListener('click', () => {
const profileNumber = parseInt(button.dataset.profile, 10);
const currentStatus = button.dataset.status || 'pending';
toggleProfileStatus(post.id, profileNumber, currentStatus);
});
});
const editPostBtn = card.querySelector('.btn-edit-post');
if (editPostBtn) {
editPostBtn.addEventListener('click', () => {
openManualPostModal({ mode: 'edit', post });
});
}
const deadlineButton = card.querySelector('.post-deadline__calendar');
if (deadlineButton) {
deadlineButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
openNativeDeadlinePicker(post, deadlineButton);
});
}
const clearDeadlineButton = card.querySelector('.post-deadline__clear');
if (clearDeadlineButton) {
clearDeadlineButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
clearDeadline(post.id);
});
}
const successCheckbox = card.querySelector('.success-checkbox-input');
if (successCheckbox) {
successCheckbox.addEventListener('change', async () => {
await toggleSuccessStatus(post.id, successCheckbox.checked);
});
}
const targetSelect = card.querySelector('.post-target__select');
if (targetSelect) {
targetSelect.dataset.originalValue = String(status.targetCount);
targetSelect.addEventListener('change', () => {
const value = parseInt(targetSelect.value, 10);
if (targetSelect.dataset.originalValue && String(value) === targetSelect.dataset.originalValue) {
targetSelect.blur();
return;
}
updateTargetInline(post.id, value, targetSelect);
targetSelect.blur();
});
}
}
function openScreenshotModal(url) {
if (!screenshotModal || !url) {
return;
}
screenshotModalLastFocus = document.activeElement;
screenshotModalImage.src = url;
resetScreenshotZoom();
screenshotModalPreviousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
screenshotModal.removeAttribute('hidden');
screenshotModal.classList.add('open');
if (screenshotModalClose) {
screenshotModalClose.focus();
}
if (screenshotModalImage.complete) {
applyScreenshotModalSize();
} else {
const handleLoad = () => {
applyScreenshotModalSize();
};
screenshotModalImage.addEventListener('load', handleLoad, { once: true });
}
}
function closeScreenshotModal() {
if (!screenshotModal) {
return;
}
if (!screenshotModal.classList.contains('open')) {
return;
}
resetScreenshotZoom();
screenshotModal.classList.remove('open');
screenshotModal.setAttribute('hidden', '');
screenshotModalImage.src = '';
document.body.style.overflow = screenshotModalPreviousOverflow;
if (screenshotModalLastFocus && typeof screenshotModalLastFocus.focus === 'function') {
screenshotModalLastFocus.focus();
}
}
function resetScreenshotZoom() {
screenshotModalZoomed = false;
if (screenshotModalContent) {
screenshotModalContent.classList.remove('zoomed');
screenshotModalContent.style.width = '';
screenshotModalContent.style.height = '';
screenshotModalContent.scrollTo({ top: 0, left: 0, behavior: 'auto' });
}
if (screenshotModalImage) {
screenshotModalImage.classList.remove('zoomed');
}
applyScreenshotModalSize();
}
function toggleScreenshotZoom() {
if (!screenshotModalContent || !screenshotModalImage) {
return;
}
screenshotModalZoomed = !screenshotModalZoomed;
screenshotModalContent.classList.toggle('zoomed', screenshotModalZoomed);
screenshotModalImage.classList.toggle('zoomed', screenshotModalZoomed);
if (screenshotModalZoomed) {
screenshotModalContent.style.width = 'min(95vw, 1300px)';
screenshotModalContent.style.height = '92vh';
screenshotModalContent.scrollTo({ top: 0, left: 0, behavior: 'auto' });
} else {
screenshotModalContent.style.width = '';
screenshotModalContent.style.height = '';
applyScreenshotModalSize();
}
}
// Create post card HTML
function createPostCard(post, status, meta = {}) {
const createdDate = formatDateTime(post.created_at) || '—';
const lastChangeDate = formatDateTime(post.last_change || post.created_at) || '—';
const baseScreenshotPath = post.screenshot_path
? (post.screenshot_path.startsWith('http')
? post.screenshot_path
: `${API_URL.replace(/\/api$/, '')}${post.screenshot_path.startsWith('/') ? '' : '/'}${post.screenshot_path}`)
: `${API_URL}/posts/${post.id}/screenshot`;
const screenshotVersion = post.last_change || post.updated_at || post.created_at || '';
const versionedScreenshotPath = screenshotVersion
? `${baseScreenshotPath}${baseScreenshotPath.includes('?') ? '&' : '?'}v=${encodeURIComponent(screenshotVersion)}`
: baseScreenshotPath;
const resolvedScreenshotPath = versionedScreenshotPath;
const screenshotHtml = `
`;
const displayIndex = typeof meta.globalIndex === 'number' ? meta.globalIndex : typeof meta.index === 'number' ? meta.index + 1 : null;
const totalFiltered = typeof meta.totalFiltered === 'number' ? meta.totalFiltered : posts.length;
const totalOverall = typeof meta.totalOverall === 'number' ? meta.totalOverall : posts.length;
const tabTotalCount = typeof meta.tabTotalCount === 'number' ? meta.tabTotalCount : posts.length;
const searchActive = !!meta.searchActive;
const counterBadge = displayIndex !== null
? `
${String(displayIndex).padStart(2, '0')}
`
: '';
const profileRowsHtml = status.profileStatuses.map((profileStatus) => {
const classes = ['profile-line', `profile-line--${profileStatus.status}`];
const isCurrentProfile = parseInt(profileStatus.profile_number, 10) === status.profileNumber;
if (isCurrentProfile) {
classes.push('profile-line--current');
}
let label = 'Wartet';
if (profileStatus.status === 'done') {
const doneDate = formatDateTime(profileStatus.checked_at);
label = doneDate ? `Erledigt (${doneDate})` : 'Erledigt';
} else if (profileStatus.status === 'available') {
label = 'Bereit';
}
const toggleLabel = profileStatus.status === 'done'
? 'Als offen markieren'
: 'Als erledigt markieren';
// Disable toggle button if post is expired
const toggleDisabled = status.isExpired ? 'disabled' : '';
const badgeHtml = isCurrentProfile ? 'Dein Profil' : '';
return `
${escapeHtml(profileStatus.profile_name)}${badgeHtml}
${escapeHtml(label)}
`;
}).join('');
const infoMessages = [];
if (status.isExpired) {
infoMessages.push('Deadline ist abgelaufen.');
}
if (!status.isCurrentProfileRequired) {
infoMessages.push('Dieses Profil muss den Beitrag nicht bestätigen.');
} else if (status.isCurrentProfileDone) {
infoMessages.push('Für dein Profil erledigt.');
} else if (status.waitingForNames.length) {
infoMessages.push(`Wartet auf: ${status.waitingForNames.join(', ')}`);
}
const infoHtml = infoMessages.length
? `
${infoMessages.map((message) => `
${escapeHtml(message)}
`).join('')}
`
: '';
const directLinkHtml = post.url
? `
`
: '';
const openButtonHtml = (status.canCurrentProfileCheck && !status.isExpired)
? `
`
: '';
const bodyClasses = ['post-body'];
if (resolvedScreenshotPath) {
bodyClasses.push('post-body--with-screenshot');
}
let creatorName = typeof post.created_by_name === 'string' && post.created_by_name.trim()
? post.created_by_name.trim()
: null;
// Remove ", Story ansehen" suffix if present
if (creatorName && creatorName.endsWith(', Story ansehen')) {
creatorName = creatorName.slice(0, -16).trim();
}
const creatorDisplay = creatorName || 'Unbekannt';
const titleText = (post.title && post.title.trim()) ? post.title.trim() : creatorDisplay;
const deadlineText = formatDeadline(post.deadline_at);
const hasDeadline = Boolean(post.deadline_at);
const isOverdue = hasDeadline && (new Date(post.deadline_at).getTime() < Date.now());
const deadlineClasses = ['post-deadline'];
let deadlineStyle = '';
if (hasDeadline) {
deadlineClasses.push('has-deadline');
// Calculate color based on time until deadline (smooth gradient)
const now = Date.now();
const deadlineTime = new Date(post.deadline_at).getTime();
const hoursUntilDeadline = (deadlineTime - now) / (1000 * 60 * 60);
let color;
if (hoursUntilDeadline < 0) {
// Overdue - dark red
color = '#dc2626';
} else {
// Smooth gradient from red (0h) to default gray (168h/7 days)
const maxHours = 168; // 7 days
const ratio = Math.min(hoursUntilDeadline / maxHours, 1);
// Color stops: red -> default gray (#4b5563)
// Red: rgb(220, 38, 38)
// Gray: rgb(75, 85, 99)
const r = Math.round(220 - (220 - 75) * ratio);
const g = Math.round(38 + (85 - 38) * ratio);
const b = Math.round(38 + (99 - 38) * ratio);
color = `rgb(${r}, ${g}, ${b})`;
}
deadlineStyle = `style="color: ${color};"`;
}
if (isOverdue) {
deadlineClasses.push('overdue');
}
return `
${screenshotHtml}
Beitrag:
#${displayIndex !== null ? displayIndex : '-'}
(${searchActive ? `${totalFiltered} Treffer von ${tabTotalCount}` : `${tabTotalCount} im Tab`} · ${totalOverall} gesamt)
Erstellt: ${escapeHtml(createdDate)}
Letzte Änderung: ${escapeHtml(lastChangeDate)}
Erstellt von: ${escapeHtml(creatorDisplay)}
${directLinkHtml}
Deadline: ${escapeHtml(deadlineText)}
${hasDeadline ? `
` : ''}
${profileRowsHtml}
${infoHtml}
${openButtonHtml}
${post.url ? `
Direkt öffnen
` : ''}
`;
}
// Open post and auto-check
async function openPost(postId) {
const post = posts.find((item) => item.id === postId);
if (!post) {
alert('Beitrag konnte nicht gefunden werden.');
return;
}
if (!post.url) {
alert('Für diesen Beitrag ist kein Direktlink vorhanden.');
return;
}
const status = computePostStatus(post);
if (!status.isCurrentProfileRequired) {
alert('Dieses Profil muss den Beitrag nicht bestätigen.');
return;
}
if (status.isCurrentProfileDone) {
window.open(post.url, '_blank');
return;
}
if (!status.canCurrentProfileCheck) {
if (status.waitingForNames.length) {
alert(`Wartet auf: ${status.waitingForNames.join(', ')}`);
} else {
alert('Der Beitrag kann aktuell nicht abgehakt werden.');
}
return;
}
try {
const response = await apiFetch(`${API_URL}/check-by-url`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: post.url,
profile_number: currentProfile
})
});
if (!response.ok) {
if (response.status === 409) {
const data = await response.json().catch(() => null);
if (data && data.error) {
alert(data.error);
return;
}
}
throw new Error('Failed to check post');
}
window.open(post.url, '_blank');
await fetchPosts({ showLoader: false });
} catch (error) {
alert('Fehler beim Abhaken des Beitrags');
console.error('Error checking post:', error);
}
}
async function toggleSuccessStatus(postId, isSuccessful) {
try {
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_successful: isSuccessful })
});
if (!response.ok) {
alert('Status konnte nicht geändert werden.');
return;
}
const updatedPost = await response.json();
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
renderPosts();
} catch (error) {
console.error('Error updating success status:', error);
alert('Status konnte nicht geändert werden.');
}
}
async function toggleProfileStatus(postId, profileNumber, currentStatus) {
if (!profileNumber) {
return;
}
const desiredStatus = currentStatus === 'done' ? 'pending' : 'done';
try {
const response = await apiFetch(`${API_URL}/posts/${postId}/profile-status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
profile_number: profileNumber,
status: desiredStatus
})
});
if (!response.ok) {
const data = await response.json().catch(() => null);
const message = data && data.error ? data.error : 'Status konnte nicht geändert werden.';
alert(message);
return;
}
const updatedPost = await response.json();
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
renderPosts();
} catch (error) {
console.error('Error updating profile status:', error);
alert('Profilstatus konnte nicht geändert werden.');
}
}
function populateManualPostForm(post) {
if (!manualPostForm || !post) {
return;
}
if (manualPostUrlInput) {
const normalizedUrl = typeof post.url === 'string' ? normalizeFacebookPostUrl(post.url) : null;
manualPostUrlInput.value = normalizedUrl || post.url || '';
manualPostUrlInput.disabled = true;
manualPostUrlInput.readOnly = true;
}
if (manualPostTitleInput) {
manualPostTitleInput.value = post.title || '';
}
if (manualPostTargetSelect) {
const targetValue = parseInt(post.target_count, 10);
manualPostTargetSelect.value = Number.isNaN(targetValue) ? '1' : String(targetValue);
}
if (manualPostCreatorInput) {
manualPostCreatorInput.value = post.created_by_name || '';
}
if (manualPostDeadlineInput) {
const existingValue = toDateTimeLocalValue(post.deadline_at);
manualPostDeadlineInput.value = existingValue || getDefaultDeadlineInputValue();
}
clearManualPostMessage();
}
function resetManualPostForm({ keepMessages = false } = {}) {
if (!manualPostForm) {
return;
}
manualPostMode = 'create';
manualPostEditingId = null;
manualPostForm.reset();
if (manualPostTargetSelect) {
manualPostTargetSelect.value = '1';
}
if (manualPostUrlInput) {
manualPostUrlInput.disabled = false;
manualPostUrlInput.readOnly = false;
manualPostUrlInput.value = '';
}
if (manualPostTitleInput) {
manualPostTitleInput.value = '';
}
if (manualPostCreatorInput) {
manualPostCreatorInput.value = '';
}
if (manualPostDeadlineInput) {
manualPostDeadlineInput.value = getDefaultDeadlineInputValue();
}
if (manualPostModalTitle) {
manualPostModalTitle.textContent = 'Beitrag hinzufügen';
}
if (manualPostSubmitButton) {
manualPostSubmitButton.textContent = 'Speichern';
}
if (!keepMessages) {
clearManualPostMessage();
}
}
async function saveDeadline(postId, deadlineIso) {
try {
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ deadline_at: deadlineIso })
});
if (!response.ok) {
const data = await response.json().catch(() => null);
const message = data && data.error ? data.error : 'Deadline konnte nicht gespeichert werden.';
alert(message);
return false;
}
const updatedPost = await response.json();
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
renderPosts();
return true;
} catch (error) {
console.error('Error updating deadline:', error);
alert('Deadline konnte nicht gespeichert werden.');
return false;
}
}
async function updateTargetInline(postId, value, selectElement) {
if (!selectElement) {
return;
}
if (Number.isNaN(value) || value < 1 || value > MAX_PROFILES) {
renderPosts();
return;
}
try {
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_count: value })
});
if (!response.ok) {
const data = await response.json().catch(() => null);
const message = data && data.error ? data.error : 'Anzahl konnte nicht gespeichert werden.';
alert(message);
if (selectElement.dataset.originalValue) {
selectElement.value = selectElement.dataset.originalValue;
}
renderPosts();
return;
}
const updatedPost = await response.json();
posts = posts.map((item) => (item.id === postId ? updatedPost : item));
renderPosts();
} catch (error) {
console.error('Error updating target count:', error);
alert('Anzahl konnte nicht gespeichert werden.');
if (selectElement.dataset.originalValue) {
selectElement.value = selectElement.dataset.originalValue;
}
renderPosts();
}
}
async function handleManualPostSubmit(event) {
event.preventDefault();
if (!manualPostForm) {
return;
}
clearManualPostMessage();
const urlValue = manualPostUrlInput ? manualPostUrlInput.value.trim() : '';
if (!urlValue) {
displayManualPostMessage('Bitte gib einen Direktlink an.', 'error');
if (manualPostUrlInput) {
manualPostUrlInput.focus();
}
return;
}
const targetValue = manualPostTargetSelect ? manualPostTargetSelect.value : '1';
const parsedTarget = parseInt(targetValue, 10);
if (Number.isNaN(parsedTarget) || parsedTarget < 1 || parsedTarget > MAX_PROFILES) {
displayManualPostMessage('Die Anzahl der benötigten Profile muss zwischen 1 und 5 liegen.', 'error');
return;
}
const creatorValue = manualPostCreatorInput ? manualPostCreatorInput.value.trim() : '';
const deadlineValue = manualPostDeadlineInput ? manualPostDeadlineInput.value : '';
const titleValue = manualPostTitleInput ? manualPostTitleInput.value.trim() : '';
const cleanedUrl = normalizeFacebookPostUrl(urlValue);
if (!cleanedUrl) {
displayManualPostMessage('Bitte gib einen gültigen Facebook-Link an.', 'error');
if (manualPostUrlInput) {
manualPostUrlInput.focus();
manualPostUrlInput.select?.();
}
return;
}
const payload = {
url: cleanedUrl,
target_count: parsedTarget
};
if (titleValue) {
payload.title = titleValue;
}
if (creatorValue) {
payload.created_by_name = creatorValue;
}
const normalizedDeadline = normalizeDeadlineInput(deadlineValue);
if (normalizedDeadline) {
payload.deadline_at = normalizedDeadline;
}
const submitButtons = manualPostForm.querySelectorAll('button, input[type="submit"]');
submitButtons.forEach((btn) => {
btn.disabled = true;
});
try {
if (manualPostMode === 'edit' && manualPostEditingId) {
const updatePayload = {
target_count: parsedTarget,
title: titleValue || ''
};
if (creatorValue || creatorValue === '') {
updatePayload.created_by_name = creatorValue || null;
}
updatePayload.deadline_at = normalizedDeadline;
const response = await apiFetch(`${API_URL}/posts/${manualPostEditingId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatePayload)
});
if (!response.ok) {
const data = await response.json().catch(() => null);
const message = data && data.error ? data.error : 'Beitrag konnte nicht aktualisiert werden.';
displayManualPostMessage(message, 'error');
return;
}
const updatedPost = await response.json();
posts = posts.map((item) => (item.id === manualPostEditingId ? updatedPost : item));
renderPosts();
closeManualPostModal();
} else {
const response = await apiFetch(`${API_URL}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const data = await response.json().catch(() => null);
if (response.status === 409 && data && data.error) {
displayManualPostMessage(data.error, 'error');
} else {
const message = data && data.error ? data.error : 'Beitrag konnte nicht erstellt werden.';
displayManualPostMessage(message, 'error');
}
return;
}
const createdPost = await response.json();
posts = [createdPost, ...posts.filter((item) => item.id !== createdPost.id)];
renderPosts();
displayManualPostMessage('Beitrag wurde erstellt.', 'success');
resetManualPostForm({ keepMessages: true });
if (manualPostUrlInput) {
manualPostUrlInput.focus();
}
}
} catch (error) {
console.error('Error creating manual post:', error);
displayManualPostMessage('Beitrag konnte nicht erstellt werden.', 'error');
} finally {
submitButtons.forEach((btn) => {
btn.disabled = false;
});
}
}
// Delete post
async function deletePost(postId) {
if (!confirm('Beitrag wirklich löschen?')) {
return;
}
try {
const response = await apiFetch(`${API_URL}/posts/${postId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete post');
}
await fetchPosts({ showLoader: false });
} catch (error) {
alert('Fehler beim Löschen des Beitrags');
console.error('Error deleting post:', error);
}
}
// Utility functions
function showLoading() {
document.getElementById('loading').style.display = 'block';
document.getElementById('postsContainer').style.display = 'none';
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
document.getElementById('postsContainer').style.display = 'block';
}
function showError(message) {
const errorEl = document.getElementById('error');
errorEl.textContent = message;
errorEl.style.display = 'block';
}
function hideError() {
document.getElementById('error').style.display = 'none';
}
function escapeHtml(unsafe) {
if (unsafe === null || unsafe === undefined) {
unsafe = '';
}
if (typeof unsafe !== 'string') {
unsafe = String(unsafe);
}
return unsafe
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Auto-check on page load if URL parameter is present
function checkAutoCheck() {
const urlParams = new URLSearchParams(window.location.search);
const autoCheckUrl = urlParams.get('check');
if (autoCheckUrl) {
// Try to check this URL automatically
apiFetch(`${API_URL}/check-by-url`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: decodeURIComponent(autoCheckUrl),
profile_number: currentProfile
})
}).then(() => {
// Remove nur den check-Parameter aus der URL
try {
const url = new URL(window.location.href);
url.searchParams.delete('check');
const paramsString = url.searchParams.toString();
const newUrl = paramsString ? `${url.pathname}?${paramsString}${url.hash}` : `${url.pathname}${url.hash}`;
window.history.replaceState({}, document.title, newUrl);
} catch (error) {
console.warn('Konnte check-Parameter nicht entfernen:', error);
}
fetchPosts({ showLoader: false });
}).catch(console.error);
}
}
if (screenshotModalClose) {
screenshotModalClose.addEventListener('click', closeScreenshotModal);
}
if (screenshotModalBackdrop) {
screenshotModalBackdrop.addEventListener('click', closeScreenshotModal);
}
if (screenshotModalImage) {
screenshotModalImage.addEventListener('click', (event) => {
event.stopPropagation();
toggleScreenshotZoom();
});
screenshotModalImage.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggleScreenshotZoom();
}
});
screenshotModalImage.setAttribute('tabindex', '0');
screenshotModalImage.setAttribute('role', 'button');
screenshotModalImage.setAttribute('aria-label', 'Screenshot vergrößern oder verkleinern');
}
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') {
return;
}
if (manualPostModal && manualPostModal.classList.contains('open')) {
event.preventDefault();
closeManualPostModal();
return;
}
if (bookmarkPanelVisible) {
event.preventDefault();
toggleBookmarkPanel(false);
return;
}
if (screenshotModal && screenshotModal.classList.contains('open')) {
if (screenshotModalZoomed) {
resetScreenshotZoom();
return;
}
closeScreenshotModal();
}
});
window.addEventListener('resize', () => {
if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) {
applyScreenshotModalSize();
}
});
// Initialize
initializeBookmarks();
loadAutoRefreshSettings();
initializeFocusParams();
initializeTabFromUrl();
loadSortMode();
resetManualPostForm();
loadProfile();
startProfilePolling();
fetchPosts();
checkAutoCheck();
startUpdatesStream();
applyAutoRefreshSettings();